PowerShell/test/tools/Modules/PSSysLog/PSSysLog.psm1
2020-02-03 09:44:10 -08:00

1135 lines
33 KiB
PowerShell
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
Set-StrictMode -Version 3.0
<#
os_log notes:
There are no public APIs on MacOS for consuming os_log data or
collecting log output in real-time. To get log data in a programmatically consumable
format, the data must first be extracted then converted to a raw text format.
Extraction requires the following steps:
1: Snapshot the current time
2: Enable persistence of PowerShell log entries
See Set-OsLogPersistence
3: Run powershell to generate the expected log output
4: Run 'log collect' to retrieve log records after a specific timestamp
from the system logs.
Note that the extracted data is still not directly consumable.
5: Run 'log show' to convert the extracted data to text and redirect it
to a file.
The --predicate can be used at this point to filter extracted records.
The typical filter is 'process == "pwsh"'
The redirected text file can then be consumed by this module.
Example command-lines:
sudo log collect --start "2018-02-07 14:33:30" --output ./system.logarchive
log show ./system.logarchive/ --info --predicate 'process == "pwsh"' >pwsh.log.txt
Parsing Notes:
* Sample contains 6.0.1 content (which is out of date) revise with 6.1.0 preview
* Ensure analytic data is considered when parsing; specifically Provider_Lifecycle:ProviderStart.Method.Informational
* Multi-line output is expected. Parsing needs to detect the timestamp at the begining
of a line and append subsequent lines to the message until the next 'log' line is found.
* Header lines need to be skipped.
Sample output from 'log show' illustrating one single-line entry and one multi-line entry.
Analytic log items are often multi-line because the message text contains newline characters.
==========
/Users/psbuildacct/system.logarchive
==========
Timestamp Thread Type Activity PID
2018-02-07 14:34:35.256501-0800 0x2a3730 Default 0x0 39437 pwsh: (libpsl-native.dylib) [com.microsoft.powershell.powershell] (v6.0.1:1:10) [Perftrack_ConsoleStartupStart:PowershellConsoleStartup.WinStart.Informational] PowerShell console is starting up
2018-02-07 14:34:35.562003-0800 0x2a373a Default 0x0 39437 pwsh: (libpsl-native.dylib) [com.microsoft.powershell.powershell] (v6.0.1:4:11) [Provider_Lifecycle:ProviderStart.Method.Informational] Provider Alias changed state to Started.
Context:
Severity = Informational
Host Name = ConsoleHost
Host Version = 6.0.1
Host ID = 964756e8-c074-4228-a54d-d410a88e8c66
Host Application = /usr/local/microsoft/powershell/6.0.1/pwsh.dll
Engine Version =
Runspace ID =
Pipeline ID =
Command Name =
Command Type =
Script Name =
Command Path =
Sequence Number = 1
User = PowerShells-MacBook\psbuildacct
Connected User =
Shell ID = Microsoft.PowerShell
User Data:
#>
#region Utilities
function Test-Sudo
{
if(-not (Test-Path -Path "env:SUDO_USER"))
{
throw "This command must be run from sudo"
}
}
function Test-MacOS
{
if (-not $IsMacOS)
{
throw "This command requires MacOS"
}
}
function Test-Linux
{
if (-not $IsLinux)
{
throw "This command requires Linux"
}
}
function Start-NativeExecution
{
param
(
[Parameter(Mandatory)]
[ValidateNotNull()]
[ScriptBlock] $command
)
$saveErrorAction = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try
{
& $command
if ($LASTEXITCODE -ne 0)
{
throw "Execution of {$command} failed with exit code $LASTEXITCODE"
}
}
finally
{
$ErrorActionPreference = $saveErrorAction
}
}
#endregion Utilities
#region SysLog support
# Defines the array indices when calling
# String.Split on a SysLog log entry
enum SysLogIds
{
Month = 0;
Day = 1;
Time = 2;
Hostname = 3;
Id = 4;
CommitId = 5;
EventId = 6;
Message = 7;
}
# Defines the array indices when calling
# String.Split on an OsLog log entry
Class OsLogIds
{
[int] $Date = 0;
[int] $Time = 1;
[int] $Thread = 2;
[int] $Type = 3;
[int] $Activity = 4;
[int] $PID = 5;
[int] $TTL = 6;
[int] $ProcessName = 7;
[int] $Module = 8;
[int] $Id = 9;
[int] $CommitId = 10;
[int] $EventId = 11;
[int] $Message = 12;
[void] UseOldIds()
{
$this.ProcessName=6;
$this.Module =7;
$this.Id =8;
$this.CommitId=9;
$this.EventId=10;
$this.Message=11;
}
}
class PSLogItem
{
[string] $LogId = [string]::Empty
[DateTime] $Timestamp = [DateTime]::Now
[string] $Hostname = [string]::Empty
[int] $ProcessId = 0
[int] $ThreadId = 0
[int] $Channel = 0
[string] $CommitId = [string]::Empty
[string] $EventId = [string]::Empty
[string] $Message = [string]::Empty
[int] $Count = 1
hidden static $monthNames = @('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun','Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')
hidden static [int] GetMonth([string] $value)
{
Set-StrictMode -Version 3.0
for ($x = 0; $x -lt [PSLogItem]::monthNames.Count; $x++)
{
[string] $monthName = [PSLogItem]::monthNames[$x]
if ($value.StartsWith($monthName, [StringComparison]::InvariantCultureIgnoreCase))
{
return $x + 1
}
}
return 1
}
static [PSLogItem] ConvertSysLog([string] $content, [string] $id, [Nullable[DateTime]] $after)
{
Set-StrictMode -Version 3.0
<#
MMM dd HH:MM:SS machinename id[PID]: (commitid:TID:CHANNEL) [EventName] Message
Expecting split to return
0: Month (abreviated)
1: DD
2: HH:MM:SS
3: hostname
4: nameid[processid]:
5: (commitid:treadid:channel)
6: [EventId]
7: Message Text
NOTE: In some cases, syslog will detect the same message being logged multiple times.
In those cases, a single message is logged in the following format
MMM dd HH:MM:SS machinename id[PID]: message repeated NNN times: [(commitid:TID:CHANNEL) [EventName] Message]
#>
# split contents into separate space delimited tokens (first 7) and leave the rest as the message.
[string[]] $parts = $content.Split(' ', 8, [System.StringSplitOptions]::RemoveEmptyEntries)
if ($parts.Count -ne 8)
{
Write-Verbose -Message "Skipping unparsable entry: $content"
return $null
}
if ($id)
{
# If the log entry doesn't have the expected $id, return null.
if ($parts[[SysLogIds]::Id].StartsWith($id, [StringComparison]::OrdinalIgnoreCase) -eq $false)
{
return $null
}
}
$now = [DateTime]::Now
$month = [PSLogItem]::GetMonth($parts[[SysLogIds]::Month])
$day = [int]::Parse($parts[[SysLogIds]::Day])
# guess at the year
$year = [DateTime]::Now.Year
if (($month -gt $now.Month) -or ($now.Month -eq $month -and $day -gt $now.Day))
{
$year--;
}
[DateTime]$time = [DateTime]::Parse($parts[[SysLogIds]::Time], [System.Globalization.CultureInfo]::InvariantCulture)
$time = [DateTime]::new($year, $month, $day, $time.Hour, $time.Minute, $time.Second)
if ($after -ne $null -and $time -lt $after)
{
# if the entry was logged prior to the expected time, return null
return $null
}
$item = [PSLogItem]::new()
$item.Timestamp = $time
$item.Message = $parts[[SysLogIds]::Message]
[char[]] $splitChars = $null
# handle log entries that have 'message repeated NNN times: ['
if ($parts[[SysLogIds]::CommitId] -eq 'message' -and $parts[[SysLogIds]::EventId] -eq 'repeated')
{
# NNN times: [ message ]
$splitChars = (' ', ':', '[')
$subparts = $item.Message.Split($splitChars, 3, [System.StringSplitOptions]::RemoveEmptyEntries)
$item.Count = [int]::Parse($subparts[0])
$value = $subparts[2]
# (commitid:TID:CHANNEL)
$start = $value.IndexOf('(')
$end = $value.IndexOf(')')
$parts[[SysLogIds]::CommitId] = $value.Substring($start, $end-$start+1)
$value = $value.Substring($end + 1)
# [eventid]
$start = $value.IndexOf('[')
$end = $value.IndexOf(']')
$parts[[SysLogIds]::EventId] = $value.Substring($start, $end-$start+1)
# message text
# NOTE: Skip the trailing ']'
$end++
$item.Message = $value.Substring($end, $value.Length - ($end + 1))
}
$item.Hostname = $parts[[SysLogIds]::Hostname]
# [EventId]
$splitChars = ('[', ']', ' ')
$item.EventId = $parts[[SysLogIds]::EventId]
$subparts = $item.EventId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)
if ($subparts.Count -eq 1)
{
$item.EventId = $subparts[0]
}
else
{
Write-Warning -Message "Could not split EventId $($item.EventId) on '[] ' Count:$($subparts.Count)"
}
# (commitid:TID:ChannelID)
[char[]] $splitChars = ('(', ')', ':', ' ')
$item.CommitId = $parts[[SysLogIds]::CommitId]
$subparts = $item.CommitId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)
if ($subparts.Count -eq 3)
{
$item.CommitId = $subparts[0]
$item.ThreadId = [int]::Parse($subparts[1], [System.Globalization.NumberStyles]::AllowHexSpecifier)
$item.Channel = [int]::Parse($subparts[2])
}
else
{
Write-Warning -Message "Could not split CommitId $($item.CommitId) on '(): ' Count:$($subparts.Count)"
}
# nameid[PID]
$splitChars = ('[',']',':')
$item.LogId = $parts[[SysLogIds]::Id]
$subparts = $item.LogId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)
if ($subparts.Count -eq 2)
{
$item.LogId = $subparts[0]
$item.ProcessId = [int]::Parse($subparts[1], [System.Globalization.NumberStyles]::AllowHexSpecifier)
}
else
{
Write-Warning -Message "Could not split LogId $($item.LogId) on '[]:' Count:$($subparts.Count)"
}
return $item
}
static [object] ConvertOsLog([string] $content, [string] $id, [Nullable[DateTime]] $after)
{
Set-StrictMode -Version 3.0
<#
Expecting split to return
0: date 2018-02-07
1: time 14:34:35.256501-0800
2: thread 0x2a3730
3: Type Default
4: activity 0x12
5: PID 39437
6: TTL (introduced in ~ 10.13) 0
7: processname pwsh:
8: sourcedll (libpsl-native.dylib)
9: log source [com.microsoft.powershell.powershell]
10: commitid:treadid:channel (v6.0.1:1:10)
11:[EventId] [Perftrack_ConsoleStartupStart:PowershellConsoleStartup.WinStart.Informational]
12:Message Text
#>
[object] $result = $content
[char[]] $splitChars = $null
do
{
# determine if the line is a log entry
# versus an overflow of a multi-line entry
# check for thread
$index = $content.IndexOf(" 0x")
if ($index -le 0)
{
# no thread value,
# assume text only
break
}
# look for a date/time stamp prior to the thread.
# if this succeeds, we'll ignore the values in split.
[DateTime] $time = [DateTime]::Now
$value = $content.Substring(0, $index).Trim()
if ([DateTime]::TryParse($value, [ref] $time) -eq $false)
{
# no timestamp,
# assume text only
break
}
if ($after -ne $null -and $time -lt $after)
{
$result = $null
break
}
$parts = $content.Split(' ', [StringSplitOptions]::RemoveEmptyEntries)
$item = [PSLogItem]::new()
$item.Count = 1
$item.Timestamp = $time
$osLogIds = [OsLogIds]::new();
$item.ProcessId = [int]::Parse($parts[$osLogIds.Pid])
# Around macOS 13, Apple added a field
# Detect if the field is the old or new field and if it is old
# Switch to the old schema
if($parts[$osLogIds.TTL] -match '\:')
{
$osLogIds.UseOldIds()
}
if($parts.Length -gt ($osLogIds.Message +1))
{
# The message got split into multiple parts, put it back together
$item.Message = $parts[$osLogIds.Message..($parts.Length-1)] -join ' '
}
else
{
$item.Message = $parts[$osLogIds.Message] -join ' '
}
# [com.microsoft.powershell.logid]
$splitChars = ('[', '.', ']')
$item.LogId = $parts[$osLogIds.Id]
if($item.LogId -notmatch '^\[com\.microsoft\.powershell')
{
Write-Verbose "Skipping logId: $($item.LogId)" -Verbose
$result = $null
break
}
$subparts = $item.LogId.Split($splitChars, [StringSplitOptions]::RemoveEmptyEntries)
if ($subparts.Length -eq 4)
{
$item.LogId = $subparts[3]
if ($null -ne $id -and $id -ne $item.LogId)
{
# this is not the log id we're looking for.
$result = $null
break
}
}
# (commitid:TID:ChannelID)
$splitChars = ('(', ')', ':', ' ')
$item.CommitId = $parts[$osLogIds.CommitId]
$subparts = $item.CommitId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)
if ($subparts.Count -eq 3)
{
$item.CommitId = $subparts[0]
$item.ThreadId = [int]::Parse($subparts[1], [System.Globalization.NumberStyles]::AllowHexSpecifier)
$item.Channel = [int]::Parse($subparts[2])
}
else
{
Write-Warning -Message "Could not split CommitId $($item.CommitId) on '(): ' Count:$($subparts.Count)"
}
# [EventId]
$splitChars = ('[', ']', ' ')
$item.EventId = $parts[$osLogIds.EventId]
$subparts = $item.EventId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)
if ($subparts.Count -eq 1)
{
$item.EventId = $subparts[0]
}
else
{
Write-Warning -Message "Could not split EventId $($item.EventId) on '[] ' Count:$($subparts.Count)"
}
$result = $item
} while ($false)
# returning [string] or [PSLogItem]
return $result
}
}
function ConvertFrom-SysLog
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]] $Content,
[string] $Id,
[Nullable[DateTime]] $After
)
Begin
{
[int] $totalWritten = 0
}
Process
{
foreach ($line in $Content)
{
[PSLogItem] $item = [PSLogItem]::ConvertSysLog($line, $id, $after)
if ($item -ne $null)
{
$totalWritten++
Write-Output $item
}
}
}
End
{
Write-Verbose "Found $totalWritten items"
}
}
<#
.SYNOPSIS
Reads log entries with the specified identifier
.PARAMETER Path
The fully qualified path to the syslog formatted file.
.PARAMETER Id
The identifier for the entries to read.
The default value is 'powershell'
.PARAMETER TotalCount
Specifies the number of items to return.
Can be used with After and Tail
.PARAMETER Tail
Specifies the number of lines from the end of a file
This value is passed through to the underlying Get-Content Cmdlet and controls
the number of lines to read from the syslog file. As a result, the number of
returned PowerShell log items may be less than this value.
.PARAMETER After
Returns items on or after the specified DateTime
Can be used with TotalCount.
.EXAMPLE
PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/powershell'
Gets all log entries from the log with the id 'powershell' from the log /var/log/powershell
.EXAMPLE
PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/syslog'
Gets all log entries from the log with the id 'powershell' from the log /var/log/syslog
.Example
PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/syslog' -Tail 200
Gets the last 200 log entries from /var/log/syslog and returns items from
this set that have the id 'powershell'.
.Example
PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/syslog' -TotalCount 200
Gets up to 200 log entries with the id 'powershell' from /var/log/syslog
.Example
PS> $time = [DateTime]::Parse('1/19/2018 1:26:49 PM')
PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/syslog' -After $time
Gets log entries with the id 'powershell' that occured on or after a specific date/time
.NOTES
This function reads syslog entries using Get-Content, filters based on the id, and
returns an object for each log entry.
#>
function Get-PSSysLog
{
[CmdletBinding(DefaultParameterSetName='All')]
param
(
[Parameter(ParameterSetName = 'All')]
[Parameter(ParameterSetName = 'After')]
[Parameter(ParameterSetName = 'Tail')]
[ValidateNotNullOrEmpty()]
[string] $Path,
[Parameter(ParameterSetName = 'All')]
[Parameter(ParameterSetName = 'After')]
[Parameter(ParameterSetName = 'Tail')]
[ValidateNotNullOrEmpty()]
[string] $Id = 'powershell',
[Parameter(ParameterSetName='All')]
[Parameter(ParameterSetName = 'After')]
[Parameter(ParameterSetName = 'Tail')]
[ValidateRange(0, [int]::MaxValue)]
[int] $TotalCount,
[Parameter(ParameterSetName = 'All')]
[Parameter(ParameterSetName = 'Tail')]
[ValidateRange(0, [int]::MaxValue)]
[int] $Tail,
[Parameter(ParameterSetName = 'All')]
[Parameter(ParameterSetName = 'After')]
[Nullable[DateTime]] $After
)
[int] $maxItems = 0
$contentParms = @{Path = $Path}
if ($PSBoundParameters.ContainsKey('Tail'))
{
$contentParms['Tail'] = $Tail
}
if ($PSBoundParameters.ContainsKey('TotalCount'))
{
$maxItems = $TotalCount
}
if ($TotalCount -eq 0)
{
Get-Content @contentParms | ConvertFrom-SysLog -After $After -Id $Id
}
else
{
[string] $filter = [string]::Format(" {0}[", $id)
Get-Content @contentParms -filter {$_.Contains($filter)} | ConvertFrom-SysLog -Id $Id -After $After | Select-Object -First $maxItems
}
}
#endregion SysLog support
#region os_log support
<#
Provides a utility class for handling single and multi-line os_log entries
#>
class LogItemBuilder
{
hidden [System.Text.StringBuilder] $sb = [System.Text.StringBuilder]::new()
hidden [PSLogItem] $item = $null
PSLogItemBuilder()
{
}
[PSLogItem] Add([object] $value)
{
[PSLogItem] $result = $null
if ($value -eq $null)
{
}
elseif ($value -is [PSLogItem])
{
# return the pending item
$result = $this.Flush()
if ($result -ne $null -and $this.sb.Length -gt 0)
{
$result.Message = $this.sb.ToString()
}
$null = $this.sb.Clear()
# save the current one to handle multiline
# message text.
$this.item = $value
}
elseif ($this.item -eq $null)
{
# Exported logs contain header lines;
# skip these.
}
else
{
# we have an item pending...
# build a multi-line message property
if ($this.sb.Length -eq 0)
{
$null = $this.sb.Append($this.item.Message)
}
$null = $this.sb.AppendLine('')
$null = $this.sb.Append($value.ToString())
}
return $result
}
[PSLogItem] Flush()
{
[PSLogItem] $result = $null
if ($this.item -ne $null)
{
$result = $this.item
$this.item = $null
if ($this.sb.Length -gt 0)
{
# rewrite the message wtih the multiple log lines
$result.Message = $this.sb.ToString()
$this.sb.Clear()
}
}
return $result
}
}
function ConvertFrom-OSLog
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]] $Content,
[string] $Id,
[Nullable[DateTime]] $After
)
Begin
{
[int] $totalWritten = 0
# NOTE: Log items can span multiple lines
[LogItemBuilder] $builder = [LogItemBuilder]::new()
}
Process
{
foreach ($line in $Content)
{
[object] $item = [PSLogItem]::ConvertOsLog($line, $id, $after)
# os_log entries can span multiple lines when new lines are
# included in the entry's message text.
# To ensure the entire log entry is processed,
# LogItemBuilder.Add will not return an item until it encounters the start
# of another log entry.
# To ensure the last item is processed, Flush needs to be called at the end of the
# pipeline. See the End block below.
$item = $builder.Add($item)
if ($item -ne $null)
{
$totalWritten++
Write-Output $item
}
}
}
End
{
$item = $builder.Flush()
if ($item -ne $null)
{
$totalWritten++
Write-Output $item
}
Write-Verbose "Found $totalWritten items"
}
}
<#
.SYNOPSIS
Reads log entries with the specified identifier
.Description
This cmdlet parses a text file exported from a MacOS os_log.
.PARAMETER Path
The fully qualified path to the os_log formatted file.
.PARAMETER Id
The identifier for the PowerShell log identity of the instance(s) producing the log content.
The default value is 'powershell'
.PARAMETER TotalCount
Specifies the maximum number of items to return.
.PARAMETER After
Returns items on or after the specified DateTime
.EXAMPLE
PS> Export-OSLog -After $timestamp | Set-Content -Path "$PSDrive/mytest.txt"
PS> Get-PSOsLog -logPath "$PSDrive/mytest.txt"
Gets all log entries from a given timestamp.
.EXAMPLE
PS> Export-OSLog -After $timestamp | Set-Content -Path "$PSDrive/mytest.txt"
PS> Get-PSOsLog -id 'mypwsh' -logPath "$PSDrive/mytest.txt" -TotalCount 200
Gets up to 200 log entries from a given timestamp with the log identity of 'mypwsh'
#>
function Get-PSOsLog
{
[CmdletBinding()]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $Path,
[string] $Id = 'powershell',
[ValidateRange(0, [int]::MaxValue)]
[int] $TotalCount,
[Nullable[DateTime]] $After
)
[int] $maxItems = 0
$contentParms = @{Path = $Path}
if ($PSBoundParameters.ContainsKey('TotalCount'))
{
$maxItems = $TotalCount
}
if ($TotalCount -eq 0)
{
Get-Content @contentParms | Where-Object {![string]::IsNullOrEmpty($_)} | ConvertFrom-OsLog -After $After -Id $Id
}
else
{
[string] $filter = [string]::Format("com.microsoft.powershell.{0}: (", $id)
Write-Warning "this code path `Get-PSOsLog -TotalCount` should not be used if the message field is needed!"
Get-Content @contentParms -filter {$_.Contains($filter)} | Where-Object {![string]::IsNullOrEmpty($_)} | ConvertFrom-OsLog -Id $Id -After $After | Select-Object -First $maxItems
}
}
<#
.SYNOPSIS
Export PowerShell os_log content as text.
.PARAMETER After
The datetime for the starting entries to export.
Log entries with a timestamp on or after this value will be returned.
.EXAMPLE
PS> $timestamp = [DateTime]::Now
PS> # perform some work...
PS> $content = (Export-PSOsLog -After $timestamp)
.EXAMPLE
PS> $timestamp = [DateTime]::Now
PS> # perform some work...
PS> Export-PSOsLog -After $timestamp | Set-Content -Path "$PSDrive/mytest.txt"
.NOTES
This command requires MacOS.
See Get-PSOsLog for parsing content from this cmdlet
#>
function Export-PSOsLog
{
[CmdletBinding(DefaultParameterSetName='default')]
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[DateTime] $After,
[string] $LogId = "powershell",
[int] $LogPid,
[Parameter(Mandatory, ParameterSetName='WaitUntil')]
[int] $TimeoutInMilliseconds,
[Parameter(Mandatory, ParameterSetName='WaitUntil')]
[int] $IntervalInMilliseconds,
[Parameter(Mandatory, ParameterSetName='WaitUntil')]
[string] $MinimumCount
)
Test-MacOS
# NOTE: The use of double quotes and single quotes for the predicate parameter
# is mandatory. Reversing the usage (e.g., single quotes around double quotes)
# causes the double quotes to be stripped breaking the predicate syntax expected
# by log show
$extraParams = @()
if($LogPid)
{
$extraParams += @(
'--predicate'
"processID == $LogPid"
)
}
if ($After -ne $null)
{
[string] $startTime = $After.ToString("yyyy-MM-dd HH:mm:ss")
$extraParams += @(
'--start'
"$startTime"
)
}
else {
$extraParams += @(
'--predicate'
"process == 'pwsh'"
)
}
Wait-UntilSuccess {
# Leaving this in an turned on by default until the tests are stabilized.
Write-Verbose "Exporting macOS logs..." -Verbose
$log = @(Start-NativeExecution -command {log show --info @extraParams})
# Only count line with `[com.microsoft.powershell` as matching rows
$logToCount = $log | Select-String -SimpleMatch '[com.microsoft.powershell'
if($logToCount.Count -ge $MinimumCount){
Write-Output $log
}
else {
throw "did not recieve at least $MinimumCount records but $($logToCount.Count) instead."
}
} -TimeoutInMilliseconds $TimeoutInMilliseconds -IntervalInMilliseconds $IntervalInMilliseconds -LogErrorSb {
$log = Start-NativeExecution -command {log show --info @extraParams}
Send-VstsLogFile -Contents $log -LogName 'Export-PSOsLog-Failure'
}
}
function Wait-UntilSuccess
{
[CmdletBinding()]
param (
[ScriptBlock]$sb,
[ScriptBlock]$LogErrorSb,
[int]$TimeoutInMilliseconds = 10000,
[int]$IntervalInMilliseconds = 10000
)
# Get the current time
$startTime = [DateTime]::Now
# Loop until the script block returns
while ($true) {
try{
return & $sb
}
catch{
# If the timeout period has passed, return false
$msPassed = ([DateTime]::Now - $startTime).TotalMilliseconds
if ($msPassed -gt $timeoutInMilliseconds) {
if($LogErrorSb)
{
try { & $LogErrorSb } catch {Write-Verbose "Logging of Error details failed with: $_" -Verbose}
}
throw
}
}
# Wait
Start-Sleep -Milliseconds $intervalInMilliseconds
}
return $true
}
<#
.SYNOPSIS
Enables or disables persistence of PowerShell logging
.PARAMETER Enable
Enable persistence of PowerShell log items
.PARAMETER Disable
Disables persistent of PowerShell log items.
This reverts persistence to the system default.
.EXAMPLE
Set-OsLogPersistence -Enable
Enables persistence of PowerShell log entries
.EXAMPLE
Set-OsLogPersistence -Disable
Reverts persistence to the default state.
.NOTES
See Get-OsLogPersistence to query the current setting.
#>
function Set-OsLogPersistence
{
[CmdletBinding()]
param
(
[Parameter(Mandatory, ParameterSetName='Enable')]
[switch] $Enable,
[Parameter(Mandatory, ParameterSetName='Disable')]
[switch] $Disable
)
Test-MacOS
Test-Sudo
if ($Enable -eq $true)
{
Write-Verbose -Message "Enabling log persistence"
Start-NativeExecution -command {log config --subsystem com.microsoft.powershell --mode=persist:info,level:info }
}
else
{
Write-Verbose -Message "Reverting log persistence to the default"
Start-NativeExecution -command {log config --subsystem com.microsoft.powershell --mode=persist:default,level:default }
}
}
<#
.SYNOPSIS
Gets the current PowerShell logging persistence setting
#>
function Get-OsLogPersistence
{
Test-MacOS
Test-Sudo
$result = Start-NativeExecution -command {log config --status --subsystem com.microsoft.powershell}
$parts = $result.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries)
if ($parts[$parts.Length - 1] -eq 'PERSIST_DEFAULT')
{
# Not configured
# Expecting a format like the following:
# Mode for 'com.microsoft.powershell' PERSIST_DEFAULT
$result = new-object PSObject -Property @{
Level = 'DEFAULT'
Persist = $parts[$parts.Length- 1]
Enabled = $false
}
}
else
{
# Expecting a format like the following:
# Mode for 'com.microsoft.powershell' INFO PERSIST_INFO
$result = new-object PSObject -Property @{
Level = $parts[$parts.Length - 2]
Persist = $parts[$parts.Length -1]
Enabled = $true
}
}
return $result
}
#region os_log support
#region eventlog support
# utility function to clear a particular log
function Clear-PSEventLog
{
param([Parameter(Mandatory=$true)][string]$Name)
wevtutil cl $Name 2> $null
}
# Waits on a windows event with a property with a particular value
function Wait-PSWinEvent
{
param(
[Parameter()]
$FilterHashtable,
[Parameter(ParameterSetName = "ByPropertyName")]
$propertyName,
[Parameter(ParameterSetName = "ByPropertyIndex")]
$propertyIndex,
[Parameter()]
$propertyValue,
[Parameter()]
$timeout = 30,
[Parameter()]
$pause = 1,
[Parameter()]
[Switch]
$All
)
    $startTime = [DateTime]::Now
$lastFoundCount = 0;
    do
    {
        Start-Sleep -Seconds $pause
$recordsToReturn = @()
        foreach ($thisRecord in (get-winevent -FilterHashtable $filterHashtable -Oldest 2> $null))
        {
if($PSCmdlet.ParameterSetName -eq "ByPropertyName")
{
if ($thisRecord."$propertyName" -like "*$propertyValue*")
{
if($All)
{
$recordsToReturn += $thisRecord
}
else
{
return $thisRecord
}
}
}
if($PSCmdlet.ParameterSetName -eq "ByPropertyIndex")
{
if ($thisRecord.Properties[$propertyIndex].Value -eq $propertyValue)
{
if($All)
{
$recordsToReturn += $thisRecord
}
else
{
return $thisRecord
}
}
}
        }
if($recordsToReturn.Count -gt 0)
{
if($recordsToReturn.Count -eq $lastFoundCount)
{
return $recordsToReturn
}
$lastFoundCount = $recordsToReturn.Count
}
    } while (([DateTime]::Now - $startTime).TotalSeconds -lt $timeout)
}
#endregion eventlog support
Export-ModuleMember -Function @(
'Clear-PSEventLog'
'Export-PSOsLog'
'Get-OsLogPersistence'
'Get-PSOsLog'
'Get-PSSysLog'
'Set-OsLogPersistence'
'Wait-PSWinEvent'
)