MegaBites-AI/Windows-powershell
0372
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 