Last active
March 7, 2022 19:13
-
-
Save Smalls1652/1e00193fb51bbf85ff18b41fe33fad0d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[CmdletBinding()] | |
param( | |
) | |
class ReleaseBuild { | |
[string]$BuildNumber | |
[System.DateTime]$ReleaseDate | |
[bool]$IsPatchTuesdayRelease | |
[string]$KbArticleId | |
[string]$KbArticleUrl | |
[string]ToString() { | |
return $this.BuildNumber | |
} | |
} | |
class WindowsReleaseInfo { | |
[string]$ReleaseName | |
[System.DateTime]$ConsumerEoLDate | |
[bool]$ConsumerIsEoL | |
[System.DateTime]$EnterpriseEoLDate | |
[bool]$EnterpriseIsEoL | |
[ReleaseBuild[]]$ReleaseBuilds | |
} | |
class WindowsReleaseEndOfLifeInfo { | |
[string]$VersionNumber | |
[System.DateTime]$EndOfLifeDate | |
} | |
function GetWindowsSupportLifecycleInfo { | |
[CmdletBinding()] | |
param( | |
[Parameter(Position = 0, Mandatory)] | |
[uri]$Uri | |
) | |
$supportLifecycleTableRegex = [System.Text.RegularExpressions.Regex]::new( | |
"<section>\n\s+<h2.+?>Releases<\/h2>\n\s+(?'tableData'(?s)<table.+?>.+?<\/table>)" | |
) | |
$supportLifecycleTableDataRegex = [System.Text.RegularExpressions.Regex]::new( | |
"<tr>\n\s+<td>Version (?'versionNumber'.{4})<\/td>\n\s+<td .+?>\n\s+.*(?'startDate'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}-\d{2}:\d{2}).+\n\s+<\/td>\n\s+<td .+?>\n\s+.*(?'endDate'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}-\d{2}:\d{2}).*\n\s+<\/td>\n\s+<\/tr>" | |
) | |
$supportLifecycleHttpClient = [System.Net.Http.HttpClient]::new() | |
$supportLifecycleHttpResponse = $supportLifecycleHttpClient.SendAsync( | |
[System.Net.Http.HttpRequestMessage]::new( | |
[System.Net.Http.HttpMethod]::Get, | |
$Uri | |
) | |
).GetAwaiter().GetResult() | |
$supportLifeCycleContents = $supportLifecycleHttpResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult() | |
$supportLifecycleHttpClient.Dispose() | |
$supportLifecycleTableRegexMatch = $supportLifecycleTableRegex.Match($supportLifeCycleContents) | |
$supportLifecycleTable = $supportLifecycleTableRegexMatch.Groups['tableData'].Value | |
$supportLifecycleTableDataMatches = $supportLifecycleTableDataRegex.Matches($supportLifecycleTable) | |
$supportLifecycleEntries = foreach ($item in $supportLifecycleTableDataMatches) { | |
[WindowsReleaseEndOfLifeInfo]@{ | |
"VersionNumber" = $item.Groups['versionNumber'].Value; | |
"EndOfLifeDate" = [System.DateTime]::Parse("$($item.Groups['endDate'].Value)") | |
} | |
} | |
return $supportLifecycleEntries | |
} | |
function GetSecondTuesdayOfMonth { | |
param( | |
[Parameter(Position = 0, Mandatory)] | |
[System.DateTime]$InputDate | |
) | |
$firstDateOfMonth = [System.DateTime]::new($InputDate.Year, $InputDate.Month, 1) | |
$firstTuesdayOfMonth = $null | |
switch ($firstDateOfMonth.DayOfWeek -eq [System.DayOfWeek]::Tuesday) { | |
$true { | |
$firstTuesdayOfMonth = $firstDateOfMonth | |
break | |
} | |
Default { | |
$firstTuesdayOfMonth = $firstDateOfMonth.AddDays([System.DayOfWeek]::Tuesday - $firstDateOfMonth.DayOfWeek) | |
break | |
} | |
} | |
$secondTuesdayOfMonth = $null | |
switch ($firstTuesdayOfMonth.Month -lt $firstDateOfMonth.Month) { | |
$true { | |
$secondTuesdayOfMonth = $firstTuesdayOfMonth.AddDays(7 * 2) | |
break | |
} | |
Default { | |
$secondTuesdayOfMonth = $firstTuesdayOfMonth.AddDays(7) | |
break | |
} | |
} | |
return $secondTuesdayOfMonth | |
} | |
$currentDateTime = [System.DateTime]::Now | |
Write-Verbose "Getting enterprise support lifecycle for each Windows 10 feature update release." | |
$windows10ConsumerSupportLifeCycleUri = [System.Uri]::new("https://docs.microsoft.com/en-us/lifecycle/products/windows-10-home-and-pro") | |
$consumerSupportLifecycleEntries = GetWindowsSupportLifecycleInfo -Uri $windows10ConsumerSupportLifeCycleUri | |
Write-Verbose "Getting enterprise support lifecycle for each Windows 10 feature update release." | |
$windows10EnterpriseSupportLifeCycleUri = [System.Uri]::new("https://docs.microsoft.com/en-us/lifecycle/products/windows-10-enterprise-and-education") | |
$enterpriseSupportLifecycleEntries = GetWindowsSupportLifecycleInfo -Uri $windows10EnterpriseSupportLifeCycleUri | |
<# | |
$windows10EnterpriseSupportLifecycleHttpClient = [System.Net.Http.HttpClient]::new() | |
$windows10EnterpriseSupportLifecycleHttpResponse = $windows10EnterpriseSupportLifecycleHttpClient.SendAsync( | |
[System.Net.Http.HttpRequestMessage]::new( | |
[System.Net.Http.HttpMethod]::Get, | |
$windows10EnterpriseSupportLifeCycleUri | |
) | |
).GetAwaiter().GetResult() | |
$windows10EnterpriseSupportLifeCycleContents = $windows10EnterpriseSupportLifecycleHttpResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult() | |
$windows10EnterpriseSupportLifecycleHttpClient.Dispose() | |
$enterpriseSupportLifecycleTableRegexMatch = $supportLifecycleTableRegex.Match($windows10EnterpriseSupportLifeCycleContents) | |
$enterpriseSupportLifecycleTable = $enterpriseSupportLifecycleTableRegexMatch.Groups['tableData'].Value | |
$enterpriseSupportLifecycleTableDataMatches = $supportLifecycleTableDataRegex.Matches($enterpriseSupportLifecycleTable) | |
$enterpriseSupportLifecycleEntries = foreach ($item in $enterpriseSupportLifecycleTableDataMatches) { | |
[pscustomobject]@{ | |
"VersionNumber" = $item.Groups['versionNumber'].Value; | |
"EndOfLifeDate" = [System.DateTime]::Parse("$($item.Groups['endDate'].Value) 00:00") | |
} | |
} | |
#> | |
Write-Verbose "Getting data from releases page." | |
$windowsReleaseInfoUri = [System.Uri]::new("https://docs.microsoft.com/en-us/windows/release-health/release-information") | |
<# | |
I haven't looked into it too much, but apparently `Invoke-WebRequest` is returning the content back as a string. | |
That's nice, but there's a lot of uncertainty in how consistent it will be. I've moved the HTTP requests to an HttpClient object. | |
$windowsReleaseInfoContents = (Invoke-WebRequest -Method "Get" -Uri $windowsReleaseInfoUri -Verbose:$false).Content | |
$windowsReleaseInfoContents = [System.Text.Encoding]::Default.GetString($windowsReleaseInfoContents) | |
#> | |
$windowsReleaseInfoHttpClient = [System.Net.Http.HttpClient]::new() | |
$windowsReleaseInfoHttpRequest = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $windowsReleaseInfoUri) | |
$windowsReleaseInfoHttpResponse = $windowsReleaseInfoHttpClient.SendAsync($windowsReleaseInfoHttpRequest).GetAwaiter().GetResult() | |
$windowsReleaseInfoContents = $windowsReleaseInfoHttpResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult() | |
$windowsReleaseInfoHttpClient.Dispose() | |
#$releasesList = [System.Collections.Generic.List[pscustomobject]]::new() | |
<# | |
$releaseRegex = [System.Text.RegularExpressions.Regex]::new( | |
"<h4 .+?>.+?<span .+>.+?<\/span> (?><strong>|)Version (?'versionName'.{4}).+?<\/strong>\r\s(?> - (?'isEoL'End of service)|).+?<\/h4>(?>\r\s<p.+?>.+?<\/p>|)\r\s(?'tableData'(?s)<table.+?>\s+.+?<\/table>)" | |
) | |
#> | |
$releaseRegex = [System.Text.RegularExpressions.Regex]::new( | |
"<strong>Version (?'versionName'.{4}) \(OS build (?'versionBuild'\d+)\)<\/strong>(?:(?:\n) - (?'isEoL'End of servicing)|)(?:(?s).+?)<table.+?>\n<tr>(?:\s*<th>.+?<\/th>){4}\n\s*<\/tr>(?'tableData'(?s).+?)\n<\/table>" | |
) | |
$releaseRegexMatches = $releaseRegex.Matches($windowsReleaseInfoContents) | |
<# | |
$tableRegex = [System.Text.RegularExpressions.Regex]::new("<tr>\r\n\s+?<td>(?'buildNumber'.+?)<\/td>\r\n\s+?<td>(?'releaseDate'.+?)<\/td>\r\n\s+?<td>(?'releaseChannel'.+?)<\/td>\r\n\s+?<td>(?><a href=\`"(?'supportArticleUrl'.+?)\`".+?>KB (?'kbArticleId'.+?)<\/a>|(?'supportArticleUrl')(?'kbArticleId'))</td>\r\n\s+?<\/tr>") | |
#> | |
$tableRegex = [System.Text.RegularExpressions.Regex]::new( | |
"<tr>\n<td>.+?<\/td>\n<td>(?'releaseDate'\d{4}-\d{2}-\d{2})<\/td>\n<td>(?'buildNumber'.+?)<\/td>\n<td>(?:<a href=\`"(?'supportArticleUrl'.+?)\`".+?>(?'kbArticleId'.+?)<\/a>)<\/td>\n<\/tr>" | |
) | |
Write-Verbose "Parsing table data." | |
$releaseRegexMatchesCount = $releaseRegexMatches.Count | |
$releaseLoopCounter = 1 | |
$releaseParseProgressSplat = @{ | |
"Activity" = "Parsing release tables"; | |
"Status" = "Progress->"; | |
"Id" = 0; | |
} | |
foreach ($regexMatch in $releaseRegexMatches) { | |
$currentOperation = "Parsing release table $($releaseLoopCounter) of $($releaseRegexMatchesCount)" | |
Write-Verbose $currentOperation | |
Write-Progress @releaseParseProgressSplat -PercentComplete ([System.Math]::Round(($releaseRegexMatchesCount / $releaseLoopCounter), 0)) -CurrentOperation $currentOperation | |
$versionName = $regexMatch.Groups['versionName'].Value | |
$tableData = $regexMatch.Groups['tableData'].Value | |
$buildList = [System.Collections.Generic.List[ReleaseBuild]]::new() | |
$tableRegexMatches = $tableRegex.Matches($tableData) | |
foreach ($buildTableRow in $tableRegexMatches) { | |
$releaseDateTime = ([System.DateTime]::Parse("$($buildTableRow.Groups['releaseDate'].Value) 00:00")) | |
$patchTuesdayDate = GetSecondTuesdayOfMonth -InputDate $releaseDateTime | |
$isPatchTuesdayRelease = $releaseDateTime -eq $patchTuesdayDate | |
$buildObj = [ReleaseBuild]@{ | |
"BuildNumber" = "10.0.$($buildTableRow.Groups['buildNumber'].Value)"; | |
"ReleaseDate" = $releaseDateTime; | |
"IsPatchTuesdayRelease" = $isPatchTuesdayRelease; | |
"KbArticleId" = $buildTableRow.Groups['kbArticleId'].Value; | |
"KbArticleUrl" = $buildTableRow.Groups['supportArticleUrl'].Value; | |
} | |
$buildList.Add($buildObj) | |
} | |
$consumerEndOfLifeDate = ($consumerSupportLifecycleEntries | Where-Object { $PSItem.VersionNumber -eq $versionName }).EndOfLifeDate | |
$consumerIsEndOfLife = $currentDateTime -gt $consumerEndOfLifeDate | |
$enterpriseEndOfLifeDate = ($enterpriseSupportLifecycleEntries | Where-Object { $PSItem.VersionNumber -eq $versionName }).EndOfLifeDate | |
$enterpriseIsEndOfLife = $currentDateTime -gt $enterpriseEndOfLifeDate | |
$releaseObj = [WindowsReleaseInfo]@{ | |
"ReleaseName" = $versionName; | |
"ConsumerEoLDate" = $consumerEndOfLifeDate; | |
"ConsumerIsEoL" = $consumerIsEndOfLife; | |
"EnterpriseEoLDate" = $enterpriseEndOfLifeDate; | |
"EnterpriseIsEoL" = $enterpriseIsEndOfLife; | |
"ReleaseBuilds" = $buildList; | |
} | |
$releaseLoopCounter++ | |
$releaseObj | |
} | |
Write-Progress @releaseParseProgressSplat -Completed |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment