Team Ai
Datasetpublic

MegaBites-AI/Windows-powershell

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes372downloads
PSSysLog.psm11158 linesDownload Raw Back to PSSysLog
1# Copyright (c) Microsoft Corporation.2# Licensed under the MIT License.3 4Set-StrictMode -Version 3.05 6<#7os_log notes:8 9There are no public APIs on MacOS for consuming os_log data or10collecting log output in real-time. To get log data in a programmatically consumable11format, the data must first be extracted then converted to a raw text format.12 13Extraction requires the following steps:14 151: Snapshot the current time16 172: Enable persistence of PowerShell log entries18   See Set-OsLogPersistence19 203: Run powershell to generate the expected log output21 224: Run 'log collect' to retrieve log records after a specific timestamp23from the system logs.24Note that the extracted data is still not directly consumable.25 265: Run 'log show' to convert the extracted data to text and redirect it27to a file.28The --predicate can be used at this point to filter extracted records.29The typical filter is 'process == "pwsh"'30 31The redirected text file can then be consumed by this module.32 33Example command-lines:34 35sudo log collect --start "2018-02-07 14:33:30" --output ./system.logarchive36log show ./system.logarchive/ --info --predicate 'process == "pwsh"' >pwsh.log.txt37 38Parsing Notes:39* Sample contains 6.0.1 content (which is out of date) revise with 6.1.0 preview40* Ensure analytic data is considered when parsing; specifically Provider_Lifecycle:ProviderStart.Method.Informational41* Multi-line output is expected. Parsing needs to detect the timestamp at the beginning42of a line and append subsequent lines to the message until the next 'log' line is found.43* Header lines need to be skipped.44 45Sample output from 'log show' illustrating one single-line entry and one multi-line entry.46Analytic log items are often multi-line because the message text contains newline characters.47 48==========49/Users/psbuildacct/system.logarchive50==========51Timestamp                       Thread     Type        Activity             PID522018-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 up532018-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.54Context:55        Severity = Informational56        Host Name = ConsoleHost57        Host Version = 6.0.158        Host ID = 964756e8-c074-4228-a54d-d410a88e8c6659        Host Application = /usr/local/microsoft/powershell/6.0.1/pwsh.dll60        Engine Version =61        Runspace ID =62        Pipeline ID =63        Command Name =64        Command Type =65        Script Name =66        Command Path =67        Sequence Number = 168        User = PowerShells-MacBook\psbuildacct69        Connected User =70        Shell ID = Microsoft.PowerShell71        User Data:72#>73 74#region Utilities75 76function Test-Sudo77{78    if(-not (Test-Path -Path "env:SUDO_USER"))79    {80        throw "This command must be run from sudo"81    }82}83 84function Test-MacOS85{86    if (-not $IsMacOS)87    {88        throw "This command requires MacOS"89    }90}91 92function Test-Linux93{94    if (-not $IsLinux)95    {96        throw "This command requires Linux"97    }98}99 100function Start-NativeExecution101{102    param103    (104        [Parameter(Mandatory)]105        [ValidateNotNull()]106        [ScriptBlock] $command107    )108    $saveErrorAction = $ErrorActionPreference109    $ErrorActionPreference = 'Continue'110 111    try112    {113        & $command114        if ($LASTEXITCODE -ne 0)115        {116            throw "Execution of {$command} failed with exit code $LASTEXITCODE"117        }118    }119    finally120    {121        $ErrorActionPreference = $saveErrorAction122    }123}124 125#endregion Utilities126 127#region SysLog support128 129# Defines the array indices when calling130# String.Split on a SysLog log entry131enum SysLogIds132{133    Month = 0;134    Day = 1;135    Time = 2;136    Hostname = 3;137    Id = 4;138    CommitId = 5;139    EventId = 6;140    Message = 7;141}142 143# Defines the array indices when calling144# String.Split on an OsLog log entry145Class OsLogIds146{147    [int] $Date = 0;148    [int] $Time = 1;149    [int] $Thread = 2;150    [int] $Type = 3;151    [int] $Activity = 4;152    [int] $PID = 5;153    [int] $TTL = 6;154    [int] $ProcessName = 7;155    [int] $Module = 8;156    [int] $Id = 9;157    [int] $CommitId = 10;158    [int] $EventId = 11;159    [int] $Message = 12;160 161    [void] UseOldIds()162    {163        $this.ProcessName=6;164        $this.Module =7;165        $this.Id =8;166        $this.CommitId=9;167        $this.EventId=10;168        $this.Message=11;169    }170}171 172 173class PSLogItem174{175    [string] $LogId = [string]::Empty176    [DateTime] $Timestamp = [DateTime]::Now177    [string] $Hostname = [string]::Empty178    [int] $ProcessId = 0179    [int] $ThreadId = 0180    [int] $Channel = 0181    [string] $CommitId = [string]::Empty182    [string] $EventId = [string]::Empty183    [string] $Message = [string]::Empty184    [int] $Count = 1185    [System.Collections.Generic.List[String]]$ParseErrors = [System.Collections.Generic.List[string]]::new()186 187    hidden static $monthNames = @('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun','Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')188 189    hidden static [int] GetMonth([string] $value)190    {191        Set-StrictMode -Version 3.0192        for ($x = 0; $x -lt [PSLogItem]::monthNames.Count; $x++)193        {194            [string] $monthName = [PSLogItem]::monthNames[$x]195            if ($value.StartsWith($monthName, [StringComparison]::InvariantCultureIgnoreCase))196            {197                return $x + 1198            }199        }200        return 1201    }202 203    static [PSLogItem] ConvertSysLog([string] $content, [string] $id, [Nullable[DateTime]] $after)204    {205        Set-StrictMode -Version 3.0206        <#207        MMM dd HH:MM:SS machinename id[PID]: (commitid:TID:CHANNEL) [EventName] Message208        Expecting split to return209        0: Month (abreviated)210        1: DD211        2: HH:MM:SS212        3: hostname213        4: nameid[processid]:214        5: (commitid:treadid:channel)215        6: [EventId]216        7: Message Text217 218        NOTE: In some cases, syslog will detect the same message being logged multiple times.219        In those cases, a single message is logged in the following format220 221        MMM dd HH:MM:SS machinename id[PID]: message repeated NNN times: [(commitid:TID:CHANNEL) [EventName] Message]222 223        Alternatively, more recent syslog daemons may change the message format to:224 225	2023-06-02T22:49:50.513735+00:00 machinename id[PID]: message repeated NNN times: [(commitid:TID:CHANNEL) [EventName] Message]226 227        the first element of the line may be converted to a datetime, which we can use to convert the input to the expected string.228        #>229 230        $firstToken = $content.split()[0]231        $dt = $firstToken -as [DateTime]232        if ($dt)233        {234            $replacement = "{0:MMM} {0:dd} {0:hh}:{0:mm}:{0:ss}" -f $dt235            $content = $content.replace($firstToken,$replacement)236        }237 238        # split contents into separate space delimited tokens (first 7) and leave the rest as the message.239        [string[]] $parts = $content.Split(' ', 8, [System.StringSplitOptions]::RemoveEmptyEntries)240 241        if ($parts.Count -ne 8)242        {243            Write-Verbose -Message "Skipping unparsable entry: $content"244            return $null245        }246 247        if ($id)248        {249            # If the log entry doesn't have the expected $id, return null.250            if ($parts[[SysLogIds]::Id].StartsWith($id, [StringComparison]::OrdinalIgnoreCase) -eq $false)251            {252                return $null253            }254        }255 256        $now = [DateTime]::Now257        $month = [PSLogItem]::GetMonth($parts[[SysLogIds]::Month])258        $day = [int]::Parse($parts[[SysLogIds]::Day])259 260        # guess at the year261        $year = [DateTime]::Now.Year262        if (($month -gt $now.Month) -or ($now.Month -eq $month -and $day -gt $now.Day))263        {264            $year--;265        }266        [DateTime]$time = [DateTime]::Parse($parts[[SysLogIds]::Time], [System.Globalization.CultureInfo]::InvariantCulture)267        $time = [DateTime]::new($year, $month, $day, $time.Hour, $time.Minute, $time.Second)268 269        if ($after -ne $null -and $time -lt $after)270        {271            # if the entry was logged prior to the expected time, return null272            return $null273        }274 275        $item = [PSLogItem]::new()276        $item.Timestamp = $time277 278        $item.Message = $parts[[SysLogIds]::Message]279 280        [char[]] $splitChars = $null281 282        # handle log entries that have 'message repeated NNN times: ['283        if ($parts[[SysLogIds]::CommitId] -eq 'message' -and $parts[[SysLogIds]::EventId] -eq 'repeated')284        {285            # NNN times: [ message ]286            $splitChars = (' ', ':', '[')287            $subparts = $item.Message.Split($splitChars, 3, [System.StringSplitOptions]::RemoveEmptyEntries)288            $item.Count = [int]::Parse($subparts[0])289            $value = $subparts[2]290 291            # (commitid:TID:CHANNEL)292            $start = $value.IndexOf('(')293            $end = $value.IndexOf(')')294            $parts[[SysLogIds]::CommitId] = $value.Substring($start, $end-$start+1)295            $value = $value.Substring($end + 1)296 297            # [eventid]298            $start = $value.IndexOf('[')299            $end = $value.IndexOf(']')300            $parts[[SysLogIds]::EventId] = $value.Substring($start, $end-$start+1)301 302            # message text303            # NOTE: Skip the trailing ']'304            $end++305            $item.Message = $value.Substring($end, $value.Length - ($end + 1))306        }307 308        $item.Hostname = $parts[[SysLogIds]::Hostname]309 310        # [EventId]311        $splitChars = ('[', ']', ' ')312        $item.EventId = $parts[[SysLogIds]::EventId]313        $subparts = $item.EventId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)314        if ($subparts.Count -eq 1)315        {316            $item.EventId = $subparts[0]317        }318        else319        {320            $item.ParseErrors.Add("Could not split EventId $($item.EventId) on '[] ' Count:$($subparts.Count) -> $content")321        }322 323        # (commitid:TID:ChannelID)324        [char[]] $splitChars = ('(', ')', ':', ' ')325        $item.CommitId = $parts[[SysLogIds]::CommitId]326        $subparts = $item.CommitId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)327        if ($subparts.Count -eq 3)328        {329            $item.CommitId = $subparts[0]330            $item.ThreadId = [int]::Parse($subparts[1], [System.Globalization.NumberStyles]::AllowHexSpecifier)331            $item.Channel = [int]::Parse($subparts[2])332        }333        else334        {335            $item.ParseErrors.Add("Could not split CommitId $($item.CommitId) on '(): ' Count:$($subparts.Count) -> $content")336        }337 338        # nameid[PID]339        $splitChars = ('[',']',':')340        $item.LogId = $parts[[SysLogIds]::Id]341        $subparts = $item.LogId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)342        if ($subparts.Count -eq 2)343        {344            $item.LogId = $subparts[0]345            $item.ProcessId = [int]::Parse($subparts[1], [System.Globalization.NumberStyles]::AllowHexSpecifier)346        }347        else348        {349            $item.ParseErrors.Add("Could not split LogId $($item.LogId) on '[]:' Count:$($subparts.Count) -> $content")350        }351 352        return $item353    }354 355    static [object] ConvertOsLog([string] $content, [string] $id, [Nullable[DateTime]] $after)356    {357        Set-StrictMode -Version 3.0358        <#359        Expecting split to return360        0: date                         2018-02-07361        1: time                         14:34:35.256501-0800362        2: thread                       0x2a3730363        3: Type                         Default364        4: activity                     0x12365        5: PID                          39437366        6: TTL (introduced in ~ 10.13)  0367        7: processname                  pwsh:368        8: sourcedll                    (libpsl-native.dylib)369        9: log source                   [com.microsoft.powershell.powershell]370        10: commitid:treadid:channel     (v6.0.1:1:10)371        11:[EventId]                    [Perftrack_ConsoleStartupStart:PowershellConsoleStartup.WinStart.Informational]372        12:Message Text373        #>374 375        [object] $result = $content376        [char[]] $splitChars = $null377        do378        {379            # determine if the line is a log entry380            # versus an overflow of a multi-line entry381 382            # check for thread383            $index = $content.IndexOf(" 0x")384            if ($index -le 0)385            {386                # no thread value,387                # assume text only388                break389            }390 391            # look for a date/time stamp prior to the thread.392            # if this succeeds, we'll ignore the values in split.393            [DateTime] $time = [DateTime]::Now394            $value = $content.Substring(0, $index).Trim()395            if ([DateTime]::TryParse($value, [ref] $time) -eq $false)396            {397                # no timestamp,398                # assume text only399                break400            }401 402            if ($after -ne $null -and $time -lt $after)403            {404                $result = $null405                break406            }407 408            $parts = $content.Split(' ', [StringSplitOptions]::RemoveEmptyEntries)409            $item = [PSLogItem]::new()410            $item.Count = 1411 412            $item.Timestamp = $time413            $osLogIds = [OsLogIds]::new();414            $item.ProcessId = [int]::Parse($parts[$osLogIds.Pid])415 416            # Around macOS 13, Apple added a field417            # Detect if the field is the old or new field and if it is old418            # Switch to the old schema419            if($parts[$osLogIds.TTL] -match '\:')420            {421                $osLogIds.UseOldIds()422            }423 424            if($parts.Length -gt ($osLogIds.Message +1))425            {426                # The message got split into multiple parts, put it back together427                $item.Message = $parts[$osLogIds.Message..($parts.Length-1)] -join ' '428            }429            else430            {431                $item.Message = $parts[$osLogIds.Message] -join ' '432            }433 434            # [com.microsoft.powershell.logid]435            $splitChars = ('[', '.', ']')436 437            $item.LogId = $parts[$osLogIds.Id]438 439            if($item.LogId -notmatch '^\[com\.microsoft\.powershell')440            {441                # this is really a lot of output, so we'll skip it for now.442                # Write-Verbose "Skipping logId: $($item.LogId)" -Verbose443                $result = $null444                break445            }446 447            $subparts = $item.LogId.Split($splitChars, [StringSplitOptions]::RemoveEmptyEntries)448            if ($subparts.Length -eq 4)449            {450                $item.LogId = $subparts[3]451                if ($null -ne $id -and $id -ne $item.LogId)452                {453                    # this is not the log id we're looking for.454                    $result = $null455                    break456                }457            }458            # (commitid:TID:ChannelID)459            $splitChars = ('(', ')', ':', ' ')460            $item.CommitId = $parts[$osLogIds.CommitId]461            $subparts = $item.CommitId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)462            if ($subparts.Count -eq 3)463            {464                $item.CommitId = $subparts[0]465                $item.ThreadId = [int]::Parse($subparts[1], [System.Globalization.NumberStyles]::AllowHexSpecifier)466                $item.Channel = [int]::Parse($subparts[2])467            }468            else469            {470                $item.ParseErrors.Add("Could not split CommitId $($item.CommitId) on '(): ' Count:$($subparts.Count)")471            }472 473            # [EventId]474            $splitChars = ('[', ']', ' ')475            $item.EventId = $parts[$osLogIds.EventId]476            $subparts = $item.EventId.Split($splitChars, [System.StringSplitOptions]::RemoveEmptyEntries)477            if ($subparts.Count -eq 1)478            {479                $item.EventId = $subparts[0]480            }481            else482            {483                $item.ParseErrors.Add("Could not split EventId $($item.EventId) on '[] ' Count:$($subparts.Count)")484            }485 486            $result = $item487        } while ($false)488 489        # returning [string] or [PSLogItem]490        return $result491    }492}493 494function ConvertFrom-SysLog495{496    [CmdletBinding()]497    param498    (499        [Parameter(Mandatory, ValueFromPipeline)]500        [string[]] $Content,501 502        [string] $Id,503 504        [Nullable[DateTime]] $After505    )506 507    Begin508    {509        [int] $totalWritten = 0510    }511 512    Process513    {514        foreach ($line in $Content)515        {516            try517            {518                [PSLogItem] $item = [PSLogItem]::ConvertSysLog($line, $id, $after)519                if ($item -ne $null)520                {521                    $totalWritten++522                    Write-Output $item523                }524            }525            catch526            {527                Write-Warning -Message "Could not convert '$line' to PSLogItem"528            }529        }530    }531    End532    {533        Write-Verbose "Found $totalWritten items"534    }535}536 537<#538.SYNOPSIS539    Reads log entries with the specified identifier540 541.PARAMETER Path542    The fully qualified path to the syslog formatted file.543 544.PARAMETER Id545    The identifier for the entries to read.546    The default value is 'powershell'547 548.PARAMETER TotalCount549    Specifies the number of items to return.550    Can be used with After and Tail551 552.PARAMETER Tail553    Specifies the number of lines from the end of a file554    This value is passed through to the underlying Get-Content Cmdlet and controls555    the number of lines to read from the syslog file. As a result, the number of556    returned PowerShell log items may be less than this value.557 558.PARAMETER After559    Returns items on or after the specified DateTime560    Can be used with TotalCount.561 562.EXAMPLE563    PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/powershell'564    Gets all log entries from the log with the id 'powershell' from the log /var/log/powershell565 566.EXAMPLE567    PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/syslog'568    Gets all log entries from the log with the id 'powershell' from the log /var/log/syslog569 570.Example571    PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/syslog' -Tail 200572    Gets the last 200 log entries from /var/log/syslog and returns items from573    this set that have the id 'powershell'.574 575.Example576    PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/syslog' -TotalCount 200577    Gets up to 200 log entries with the id 'powershell' from /var/log/syslog578 579.Example580    PS> $time = [DateTime]::Parse('1/19/2018 1:26:49 PM')581    PS> Get-PSSysLog -id 'powershell' -logPath '/var/log/syslog' -After $time582 583    Gets log entries with the id 'powershell' that occurred on or after a specific date/time584 585.NOTES586    This function reads syslog entries using Get-Content, filters based on the id, and587    returns an object for each log entry.588#>589function Get-PSSysLog590{591    [CmdletBinding(DefaultParameterSetName='All')]592    param593    (594        [Parameter(ParameterSetName = 'All')]595        [Parameter(ParameterSetName = 'After')]596        [Parameter(ParameterSetName = 'Tail')]597        [ValidateNotNullOrEmpty()]598        [string] $Path,599 600        [Parameter(ParameterSetName = 'All')]601        [Parameter(ParameterSetName = 'After')]602        [Parameter(ParameterSetName = 'Tail')]603        [ValidateNotNullOrEmpty()]604        [string] $Id = 'powershell',605 606        [Parameter(ParameterSetName='All')]607        [Parameter(ParameterSetName = 'After')]608        [Parameter(ParameterSetName = 'Tail')]609        [ValidateRange(0, [int]::MaxValue)]610        [int] $TotalCount,611 612        [Parameter(ParameterSetName = 'All')]613        [Parameter(ParameterSetName = 'Tail')]614        [ValidateRange(0, [int]::MaxValue)]615        [int] $Tail,616 617        [Parameter(ParameterSetName = 'All')]618        [Parameter(ParameterSetName = 'After')]619        [Nullable[DateTime]] $After620    )621 622    [int] $maxItems = 0623    $contentParms = @{Path = $Path}624    if ($PSBoundParameters.ContainsKey('Tail'))625    {626        $contentParms['Tail'] = $Tail627    }628 629    if ($PSBoundParameters.ContainsKey('TotalCount'))630    {631        $maxItems = $TotalCount632    }633 634    if ($TotalCount -eq 0)635    {636        Get-Content @contentParms | ConvertFrom-SysLog -After $After -Id $Id637    }638    else639    {640        [string] $filter = [string]::Format(" {0}[", $id)641        Get-Content @contentParms -Filter {$_.Contains($filter)} | ConvertFrom-SysLog -Id $Id -After $After | Select-Object -First $maxItems642    }643}644 645#endregion SysLog support646 647#region os_log support648 649<#650    Provides a utility class for handling single and multi-line os_log entries651#>652class LogItemBuilder653{654    hidden [System.Text.StringBuilder] $sb = [System.Text.StringBuilder]::new()655    hidden [PSLogItem] $item = $null656 657    PSLogItemBuilder()658    {659    }660 661    [PSLogItem] Add([object] $value)662    {663        [PSLogItem] $result = $null664        if ($value -eq $null)665        {666        }667        elseif ($value -is [PSLogItem])668        {669            # return the pending item670            $result = $this.Flush()671            if ($result -ne $null -and $this.sb.Length -gt 0)672            {673                $result.Message = $this.sb.ToString()674            }675 676            $null = $this.sb.Clear()677            # save the current one to handle multiline678            # message text.679            $this.item = $value680        }681        elseif ($this.item -eq $null)682        {683            # Exported logs contain header lines;684            # skip these.685        }686        else687        {688            # we have an item pending...689            # build a multi-line message property690            if ($this.sb.Length -eq 0)691            {692                $null = $this.sb.Append($this.item.Message)693            }694            $null = $this.sb.AppendLine('')695            $null = $this.sb.Append($value.ToString())696        }697 698        return $result699    }700 701    [PSLogItem] Flush()702    {703        [PSLogItem] $result = $null704        if ($this.item -ne $null)705        {706            $result = $this.item707            $this.item = $null708            if ($this.sb.Length -gt 0)709            {710                # rewrite the message wtih the multiple log lines711                $result.Message = $this.sb.ToString()712                $this.sb.Clear()713            }714        }715        return $result716    }717}718 719function ConvertFrom-OSLog720{721    [CmdletBinding()]722    param723    (724        [Parameter(Mandatory, ValueFromPipeline)]725        [string[]] $Content,726 727        [string] $Id,728 729        [Nullable[DateTime]] $After730    )731 732    Begin733    {734        [int] $totalWritten = 0735        # NOTE: Log items can span multiple lines736        [LogItemBuilder] $builder = [LogItemBuilder]::new()737    }738 739    Process740    {741        foreach ($line in $Content)742        {743            [object] $item = [PSLogItem]::ConvertOsLog($line, $id, $after)744 745            # os_log entries can span multiple lines when new lines are746            # included in the entry's message text.747            # To ensure the entire log entry is processed,748            #    LogItemBuilder.Add will not return an item until it encounters the start749            #    of another log entry.750            # To ensure the last item is processed, Flush needs to be called at the end of the751            #    pipeline. See the End block below.752            $item = $builder.Add($item)753            if ($item -ne $null)754            {755                $totalWritten++756                Write-Output $item757            }758        }759    }760    End761    {762        $item = $builder.Flush()763        if ($item -ne $null)764        {765            $totalWritten++766            Write-Output $item767        }768        Write-Verbose "Found $totalWritten items"769    }770}771 772<#773.SYNOPSIS774    Reads log entries with the specified identifier775 776.Description777    This cmdlet parses a text file exported from a MacOS os_log.778 779.PARAMETER Path780    The fully qualified path to the os_log formatted file.781 782.PARAMETER Id783    The identifier for the PowerShell log identity of the instance(s) producing the log content.784    The default value is 'powershell'785 786.PARAMETER TotalCount787    Specifies the maximum number of items to return.788 789.PARAMETER After790    Returns items on or after the specified DateTime791 792.EXAMPLE793    PS> Export-OSLog -After $timestamp | Set-Content -Path "$PSDrive/mytest.txt"794    PS> Get-PSOsLog -logPath "$PSDrive/mytest.txt"795 796    Gets all log entries from a given timestamp.797 798.EXAMPLE799    PS> Export-OSLog -After $timestamp | Set-Content -Path "$PSDrive/mytest.txt"800    PS> Get-PSOsLog -id 'mypwsh' -logPath "$PSDrive/mytest.txt" -TotalCount 200801 802    Gets up to 200 log entries from a given timestamp with the log identity of 'mypwsh'803#>804function Get-PSOsLog805{806    [CmdletBinding()]807    param808    (809        [Parameter(Mandatory)]810        [ValidateNotNullOrEmpty()]811        [string] $Path,812 813        [string] $Id = 'powershell',814 815        [ValidateRange(0, [int]::MaxValue)]816        [int] $TotalCount,817 818        [Nullable[DateTime]] $After819    )820 821    [int] $maxItems = 0822    $contentParms = @{Path = $Path}823 824    if ($PSBoundParameters.ContainsKey('TotalCount'))825    {826        $maxItems = $TotalCount827    }828 829    if ($TotalCount -eq 0)830    {831        Get-Content @contentParms | Where-Object {![string]::IsNullOrEmpty($_)} | ConvertFrom-OsLog -After $After -Id $Id832    }833    else834    {835        [string] $filter = [string]::Format("com.microsoft.powershell.{0}: (", $id)836        Write-Warning "this code path `Get-PSOsLog -TotalCount` should not be used if the message field is needed!"837        Get-Content @contentParms -Filter {$_.Contains($filter)} | Where-Object {![string]::IsNullOrEmpty($_)} | ConvertFrom-OsLog -Id $Id -After $After | Select-Object -First $maxItems838    }839}840 841<#842.SYNOPSIS843    Export PowerShell os_log content as text.844 845.PARAMETER After846    The datetime for the starting entries to export.847    Log entries with a timestamp on or after this value will be returned.848 849.EXAMPLE850    PS> $timestamp = [DateTime]::Now851    PS> # perform some work...852    PS> $content = (Export-PSOsLog -After $timestamp)853 854.EXAMPLE855    PS> $timestamp = [DateTime]::Now856    PS> # perform some work...857    PS> Export-PSOsLog -After $timestamp | Set-Content -Path "$PSDrive/mytest.txt"858 859.NOTES860    This command requires MacOS.861    See Get-PSOsLog for parsing content from this cmdlet862#>863function Export-PSOsLog864{865    [CmdletBinding(DefaultParameterSetName='default')]866    param867    (868        [Parameter(Mandatory)]869        [ValidateNotNullOrEmpty()]870        [DateTime] $After,871 872        [string] $LogId = "powershell",873 874        [int] $LogPid,875 876        [Parameter(Mandatory, ParameterSetName='WaitUntil')]877        [int] $TimeoutInMilliseconds,878 879        [Parameter(Mandatory, ParameterSetName='WaitUntil')]880        [int] $IntervalInMilliseconds,881 882        [Parameter(Mandatory, ParameterSetName='WaitUntil')]883        [string] $MinimumCount884    )885 886    Test-MacOS887 888    # NOTE: The use of double quotes and single quotes for the predicate parameter889    # is mandatory. Reversing the usage (e.g., single quotes around double quotes)890    # causes the double quotes to be stripped breaking the predicate syntax expected891    # by log show892    $extraParams = @()893    if($LogPid)894    {895        $extraParams += @(896            '--predicate'897            "processID == $LogPid"898        )899    }900    if ($After -ne $null)901    {902        [string] $startTime = $After.ToString("yyyy-MM-dd HH:mm:ss")903        $extraParams += @(904            '--start'905            "$startTime"906        )907    }908    else {909        $extraParams += @(910            '--predicate'911            "process == 'pwsh'"912        )913    }914 915    Wait-UntilSuccess {916        # Leaving this in an turned on by default until the tests are stabilized.917        Write-Verbose "Exporting macOS logs..." -Verbose918        $log = @(Start-NativeExecution -command {log show --info @extraParams})919 920        # Only count line with `[com.microsoft.powershell` as matching rows921        $logToCount = $log | Select-String -SimpleMatch '[com.microsoft.powershell'922 923        if($logToCount.Count -ge $MinimumCount){924            Write-Output $log925        }926        else {927            throw "did not receive at least $MinimumCount records but $($logToCount.Count) instead."928        }929    } -TimeoutInMilliseconds $TimeoutInMilliseconds -IntervalInMilliseconds $IntervalInMilliseconds -LogErrorSb {930        $log = Start-NativeExecution -command {log show --info @extraParams}931        Send-VstsLogFile -Contents $log -LogName 'Export-PSOsLog-Failure'932    }933}934 935function Wait-UntilSuccess936{937    [CmdletBinding()]938    param (939        [ScriptBlock]$sb,940        [ScriptBlock]$LogErrorSb,941        [int]$TimeoutInMilliseconds = 10000,942        [int]$IntervalInMilliseconds = 10000943        )944    # Get the current time945    $startTime = [DateTime]::Now946 947    # Loop until the script block returns948    while ($true) {949        try{950            return & $sb951        }952        catch{953            # If the timeout period has passed, return false954            $msPassed = ([DateTime]::Now - $startTime).TotalMilliseconds955            if ($msPassed -gt $timeoutInMilliseconds) {956                if($LogErrorSb)957                {958                    try { & $LogErrorSb } catch {Write-Verbose "Logging of Error details failed with: $_" -Verbose}959                }960                throw961            }962        }963 964        # Wait965        Start-Sleep -Milliseconds $intervalInMilliseconds966    }967    return $true968}969 970<#971.SYNOPSIS972    Enables or disables persistence of PowerShell logging973 974.PARAMETER Enable975    Enable persistence of PowerShell log items976 977.PARAMETER Disable978    Disables persistent of PowerShell log items.979    This reverts persistence to the system default.980 981.EXAMPLE982    Set-OsLogPersistence -Enable983    Enables persistence of PowerShell log entries984 985.EXAMPLE986    Set-OsLogPersistence -Disable987    Reverts persistence to the default state.988 989.NOTES990    See Get-OsLogPersistence to query the current setting.991#>992function Set-OsLogPersistence993{994    [CmdletBinding()]995    param996    (997        [Parameter(Mandatory, ParameterSetName='Enable')]998        [switch] $Enable,999 1000        [Parameter(Mandatory, ParameterSetName='Disable')]1001        [switch] $Disable1002    )1003    Test-MacOS1004    Test-Sudo1005 1006    if ($Enable -eq $true)1007    {1008        Write-Verbose -Message "Enabling log persistence"1009        Start-NativeExecution -command {log config --subsystem com.microsoft.powershell --mode=persist:info,level:info }1010    }1011    else1012    {1013        Write-Verbose -Message "Reverting log persistence to the default"1014        Start-NativeExecution -command {log config --subsystem com.microsoft.powershell --mode=persist:default,level:default }1015    }1016}1017 1018<#1019.SYNOPSIS1020   Gets the current PowerShell logging persistence setting1021#>1022function Get-OsLogPersistence1023{1024    Test-MacOS1025    Test-Sudo1026 1027    $result = Start-NativeExecution -command {log config --status --subsystem com.microsoft.powershell}1028    $parts = $result.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries)1029 1030    if ($parts[$parts.Length - 1] -eq 'PERSIST_DEFAULT')1031    {1032        # Not configured1033        # Expecting a format like the following:1034        # Mode for 'com.microsoft.powershell'  PERSIST_DEFAULT1035        $result = New-Object PSObject -Property @{1036            Level = 'DEFAULT'1037            Persist = $parts[$parts.Length- 1]1038            Enabled = $false1039        }1040    }1041    else1042    {1043        # Expecting a format like the following:1044        # Mode for 'com.microsoft.powershell'  INFO PERSIST_INFO1045        $result = New-Object PSObject -Property @{1046            Level = $parts[$parts.Length - 2]1047            Persist = $parts[$parts.Length -1]1048            Enabled = $true1049        }1050    }1051    return $result1052}1053 1054#region os_log support1055 1056 1057#region eventlog support1058 1059# utility function to clear a particular log1060function Clear-PSEventLog1061{1062   param([Parameter(Mandatory=$true)][string]$Name)1063 1064   wevtutil cl $Name 2> $null1065}1066 1067# Waits on a windows event with a property with a particular value1068function Wait-PSWinEvent1069{1070    param(1071        [Parameter()]1072        $FilterHashtable,1073 1074        [Parameter(ParameterSetName = "ByPropertyName")]1075        $propertyName,1076 1077        [Parameter(ParameterSetName = "ByPropertyIndex")]1078        $propertyIndex,1079 1080        [Parameter()]1081        $propertyValue,1082 1083        [Parameter()]1084        $timeout = 30,1085 1086        [Parameter()]1087        $pause = 1,1088 1089        [Parameter()]1090        [Switch]1091        $All1092    )1093 1094    $startTime = [DateTime]::Now1095    $lastFoundCount = 0;1096 1097    do1098    {1099        Start-Sleep -Seconds $pause1100 1101        $recordsToReturn = @()1102 1103        foreach ($thisRecord in (Get-WinEvent -FilterHashtable $filterHashtable -Oldest 2> $null))1104        {1105            if($PSCmdlet.ParameterSetName -eq "ByPropertyName")1106            {1107                if ($thisRecord."$propertyName" -like "*$propertyValue*")1108                {1109                    if($All)1110                    {1111                        $recordsToReturn += $thisRecord1112                    }1113                    else1114                    {1115                        return $thisRecord1116                    }1117                }1118            }1119 1120            if($PSCmdlet.ParameterSetName -eq "ByPropertyIndex")1121            {1122                if ($thisRecord.Properties[$propertyIndex].Value -eq $propertyValue)1123                {1124                    if($All)1125                    {1126                        $recordsToReturn += $thisRecord1127                    }1128                    else1129                    {1130                        return $thisRecord1131                    }1132                }1133            }1134        }1135 1136        if($recordsToReturn.Count -gt 0)1137        {1138            if($recordsToReturn.Count -eq $lastFoundCount)1139            {1140               return $recordsToReturn1141            }1142 1143            $lastFoundCount = $recordsToReturn.Count1144        }1145    } while (([DateTime]::Now - $startTime).TotalSeconds -lt $timeout)1146}1147#endregion eventlog support1148 1149Export-ModuleMember -Function @(1150    'Clear-PSEventLog'1151    'Export-PSOsLog'1152    'Get-OsLogPersistence'1153    'Get-PSOsLog'1154    'Get-PSSysLog'1155    'Set-OsLogPersistence'1156    'Wait-PSWinEvent'1157)1158