Skip to content

Instantly share code, notes, and snippets.

@Smalls1652
Last active March 7, 2022 19:13
Show Gist options
  • Save Smalls1652/1e00193fb51bbf85ff18b41fe33fad0d to your computer and use it in GitHub Desktop.
Save Smalls1652/1e00193fb51bbf85ff18b41fe33fad0d to your computer and use it in GitHub Desktop.
[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