From 81541a83f2fc49402b5e56c9534c8c8da04888ab Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 2 Nov 2024 16:29:15 +0100 Subject: [PATCH 01/33] Add whatif support --- resources/PythonPip3Dsc/PythonPip3Dsc.psd1 | 163 +++++++++++---------- resources/PythonPip3Dsc/PythonPip3Dsc.psm1 | 120 ++++++++++++++- 2 files changed, 196 insertions(+), 87 deletions(-) diff --git a/resources/PythonPip3Dsc/PythonPip3Dsc.psd1 b/resources/PythonPip3Dsc/PythonPip3Dsc.psd1 index cebf75ed..a95da903 100644 --- a/resources/PythonPip3Dsc/PythonPip3Dsc.psd1 +++ b/resources/PythonPip3Dsc/PythonPip3Dsc.psd1 @@ -8,127 +8,128 @@ @{ -# Script module or binary module file associated with this manifest. -RootModule = 'PythonPip3Dsc.psm1' + # Script module or binary module file associated with this manifest. + RootModule = 'PythonPip3Dsc.psm1' -# Version number of this module. -ModuleVersion = '0.1.0' + # Version number of this module. + ModuleVersion = '0.1.0' -# Supported PSEditions -# CompatiblePSEditions = @() + # Supported PSEditions + # CompatiblePSEditions = @() -# ID used to uniquely identify this module -GUID = 'bc1cab01-7e6f-4bba-a6ec-d77d0ffe91c7' + # ID used to uniquely identify this module + GUID = 'bc1cab01-7e6f-4bba-a6ec-d77d0ffe91c7' -# Author of this module -Author = 'DscSamples' + # Author of this module + Author = 'DscSamples' -# Company or vendor of this module -# CompanyName = '' + # Company or vendor of this module + # CompanyName = '' -# Copyright statement for this module -# Copyright = '' + # Copyright statement for this module + # Copyright = '' -# Description of the functionality provided by this module -Description = 'DSC Resource for Python pip3' + # Description of the functionality provided by this module + Description = 'DSC Resource for Python pip3' -# Minimum version of the PowerShell engine required by this module -# PowerShellVersion = '' + # Minimum version of the PowerShell engine required by this module + # PowerShellVersion = '' -# Name of the PowerShell host required by this module -# PowerShellHostName = '' + # Name of the PowerShell host required by this module + # PowerShellHostName = '' -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# ClrVersion = '' + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -# FunctionsToExport = @() + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + # FunctionsToExport = @() -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -# CmdletsToExport = @() + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + # CmdletsToExport = @() -# Variables to export from this module -# VariablesToExport = '*' + # Variables to export from this module + # VariablesToExport = '*' -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -# AliasesToExport = @() + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + # AliasesToExport = @() -# DSC resources to export from this module -DscResourcesToExport = @( - 'Pip3Package' -) + # DSC resources to export from this module + DscResourcesToExport = @( + 'Pip3Package' + ) -# List of all modules packaged with this module -# ModuleList = @() + # List of all modules packaged with this module + # ModuleList = @() -# List of all files packaged with this module -# FileList = @() + # List of all files packaged with this module + # FileList = @() -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ - PSData = @{ + PSData = @{ - # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('PSDscResource_Pip3Package') + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('PSDscResource_Pip3Package') - # A URL to the license for this module. - LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' - # A URL to the main website for this project. - ProjectUri = 'https://github.com/microsoft/winget-dsc' + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-dsc' - # A URL to an icon representing this module. - # IconUri = '' + # A URL to an icon representing this module. + # IconUri = '' - # ReleaseNotes of this module - # ReleaseNotes = '' + # ReleaseNotes of this module + # ReleaseNotes = '' - # Prerelease string of this module - Prerelease = 'alpha' + # Prerelease string of this module + Prerelease = 'alpha' - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false - # External dependent modules of this module - # ExternalModuleDependencies = @() + # External dependent modules of this module + # ExternalModuleDependencies = @() + DscCapabilities = @('Get', 'Set', 'Test', 'Export', 'WhatIf') - } # End of PSData hashtable + } # End of PSData hashtable -} # End of PrivateData hashtable + } # End of PrivateData hashtable -# HelpInfo URI of this module -# HelpInfoURI = '' + # HelpInfo URI of this module + # HelpInfoURI = '' -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' } diff --git a/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 b/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 index 1a138b36..4e884164 100644 --- a/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 +++ b/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 @@ -4,6 +4,77 @@ using namespace System.Collections.Generic #region Functions +function Invoke-Process +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$FilePath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$ArgumentList, + + [ValidateSet("Full", "StdOut", "StdErr", "ExitCode", "None")] + [string]$DisplayLevel + ) + + try + { + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $FilePath + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.WindowStyle = 'Hidden' + $pinfo.CreateNoWindow = $true + $pinfo.Arguments = $ArgumentList + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + + $stOut = @() + while (-not $p.StandardOutput.EndOfStream) + { + $stOut += $p.StandardOutput.ReadLine() + } + + $stErr = @() + while (-not $p.StandardError.EndOfStream) + { + $stErr += $p.StandardError.ReadLine() + } + + $result = [pscustomobject]@{ + Title = ($MyInvocation.MyCommand).Name + Command = $FilePath + Arguments = $ArgumentList + StdOut = $stOut + StdErr = $stErr + ExitCode = $p.ExitCode + } + + $p.WaitForExit() + + if (-not([string]::IsNullOrEmpty($DisplayLevel))) + { + switch ($DisplayLevel) + { + "Full" { return $result; break } + "StdOut" { return $result.StdOut; break } + "StdErr" { return $result.StdErr; break } + "ExitCode" { return $result.ExitCode; break } + } + } + } + catch + { + exit 1 + } +} + function Get-Pip3Path { if ($IsWindows) @@ -106,7 +177,10 @@ function Get-PackageNameWithVersion [string]$Version, [Parameter()] - [switch]$IsUpdate + [switch]$IsUpdate, + + [Parameter()] + [switch]$DryRun ) if ($PSBoundParameters.ContainsKey('Version') -and -not ([string]::IsNullOrEmpty($Version))) @@ -130,7 +204,10 @@ function Invoke-Pip3Install [string]$Version, [Parameter()] - [switch]$IsUpdate + [switch]$IsUpdate, + + [Parameter()] + [switch]$DryRun ) $command = [List[string]]::new() @@ -140,8 +217,16 @@ function Invoke-Pip3Install { $command.Add("--force-reinstall") } + if ($DryRun.IsPresent) + { + $command.Add("--dry-run") + } + $command.Add($Arguments) - return Invoke-Pip3 -command $command + Write-Verbose -Message "Executing 'pip' install with command: $command" + $result = Invoke-Pip3 -command $command + + return $result } function Invoke-Pip3Uninstall @@ -164,6 +249,7 @@ function Invoke-Pip3Uninstall # '--yes' is needed to ignore confrimation required for uninstalls $command.Add("--yes") + Write-Verbose -Message "Executing 'pip' uninstall with command: $command" return Invoke-Pip3 -command $command } @@ -249,11 +335,11 @@ function Invoke-Pip3 if ($global:usePip3Exe) { - return Start-Process -FilePath $global:pip3ExePath -ArgumentList $command -Wait -PassThru -WindowStyle Hidden + return Invoke-Process -FilePath $global:pip3ExePath -ArgumentList $command -DisplayLevel Full } else { - return Start-Process -FilePath pip3 -ArgumentList $command -Wait -PassThru -WindowStyle hidden + return Invoke-Process -FilePath pip3 -ArgumentList $command -DisplayLevel Full } } @@ -399,6 +485,28 @@ class Pip3Package } } + [string] WhatIf() + { + if ($this.Exist) + { + $whatIfState = Invoke-Pip3Install -PackageName $this.PackageName -Version $this.Version -Arguments $this.Arguments -DryRun + + $out = @{ + PackageName = $this.PackageName + _metaData = @{ + whatIf = $whatIfState.StdOut + } + } + } + else + { + # Uninstall does not have --dry-run param + $out = @{} + } + + return ($out | ConvertTo-Json -Depth 10 -Compress) + } + static [Pip3Package[]] Export() { $packages = GetInstalledPip3Packages @@ -436,4 +544,4 @@ class Pip3Package #endRegion Pip3Package Helper functions } -#endregion DSCResources +#endregion DSCResources \ No newline at end of file From b180c3acc3f0df0a3cf70cc30defdf71e775a86e Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 05:44:23 +0100 Subject: [PATCH 02/33] Initial setup of Windows Setting Language --- .../Microsoft.Windows.Setting.Language.psd1 | 132 +++++++++++++++ .../Microsoft.Windows.Setting.Language.psm1 | 156 ++++++++++++++++++ ...crosoft.Windows.Setting.Language.Tests.ps1 | 22 +++ utilities/scripts/New-DscResourceModule.ps1 | 98 +++++++++++ 4 files changed, 408 insertions(+) create mode 100644 resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psd1 create mode 100644 resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 create mode 100644 tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 create mode 100644 utilities/scripts/New-DscResourceModule.ps1 diff --git a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psd1 b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psd1 new file mode 100644 index 00000000..e98c3d4f --- /dev/null +++ b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psd1 @@ -0,0 +1,132 @@ +# +# Module manifest for module 'Microsoft.Windows.Setting.Language' +# +# Generated by: Microsoft Corporation +# +# Generated on: 04/11/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'Microsoft.Windows.Setting.Language.psm1' + +# Version number of this module. +ModuleVersion = '0.1.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '6ab8bbf6-ce28-4d33-a3ce-04c1cc16f139' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'DSC Resource for Windows Setting Language' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.2' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = '*' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-dsc' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + Prerelease = 'alpha' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 new file mode 100644 index 00000000..b8c26da5 --- /dev/null +++ b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 @@ -0,0 +1,156 @@ +$global:LocaleNameRegistryPath = "HKCU:\Control Panel\International" +$global:LocaleUserProfilePath = "HKCU:\Control Panel\International\User Profile" + +#region Functions +function Get-OsBuildVersion +{ + return [System.Environment]::OSVersion.Version.Build +} + +function Set-LocaleByOs +{ + param ( + [Parameter(Mandatory = $true)] + [string]$LocaleName + ) + + if (Test-Win11OrServer2022) + { + if (Test-LocaleByOs -LocaleName $LocaleName) + { + Set-WinUserLanguageList -Language $LocaleName + } + } + # TODO: Add support for older OS versions + # Challenging to get input method tips for older OS versions + else + { + Throw "This module only supports Windows 11 and Windows Server 2022." + } +} + +function Test-Win11OrServer2022 +{ + $osBuildVersion = Get-OsBuildVersion + + if ($osBuildVersion -gt 26100 -or $osBuildVersion -gt 20348) + { + return $true + } + + return $false +} + +function Test-LocaleByOs +{ + param ( + [Parameter(Mandatory = $true)] + [string]$LocaleName + ) + + $osBuildVersion = Get-OsBuildVersion + + if ($osBuildVersion -gt 26100 -or $osBuildVersion -gt 20348) + { + $languageList = Get-WinUserLanguageList + if ($languageList.Language -in $LocaleName) + { + return $true + } + else + { + Throw "Language `"$($LocaleName)`" is not installed. Please make sure the language is installed on the system first." + } + } +} + +function TryGetRegistryValue +{ + param ( + [Parameter(Mandatory = $true)] + [string]$Key, + + [Parameter(Mandatory = $true)] + [string]$Property + ) + + if (Test-Path -Path $Key) + { + try + { + return (Get-ItemProperty -Path $Key | Select-Object -ExpandProperty $Property) + } + catch + { + Write-Verbose "Property `"$($Property)`" could not be found." + } + } + else + { + Write-Verbose "Registry key does not exist." + } +} +#endregion Functions + +#region Classes +[DscResource()] +class Language +{ + + [DscProperty(Key)] + [string] $LocaleName + + [DscProperty()] + [bool] $Exist = $true + + hidden [string] $KeyName = 'LocaleName' + + Language() + { + $this + } + + [Language] Get() + { + $currentState = [Language]::new() + + # check if user profile contains language + $userProfileLanguageDict = TryGetRegistryValue -Key (Join-path $global:LocaleUserProfilePath $this.LocaleName) -Property 'CachedLanguageName' + if ((TryGetRegistryValue -Key $global:LocaleNameRegistryPath -Property $this.KeyName) -ne $this.LocaleName -and ($null -ne $userProfileLanguageDict)) + { + $currentState.Exist = $false + return $currentState + } + + return @{ + LocaleName = $this.LocaleName + Exist = $true + } + } + + [void] Set() + { + if ($this.Test()) + { + return + } + + # TODO: How do we handle sign out and sign in? + Set-LocaleByOs -LocaleName $this.LocaleName + + # TODO: Exist does not make sense here, we always want a language to exist + } + + [bool] Test() + { + $currentState = $this.Get() + + if ($currentState.Exist -ne $this.Exist) + { + return $false + } + + return $true + } +} +#endRegion classes diff --git a/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 b/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 new file mode 100644 index 00000000..2e819b19 --- /dev/null +++ b/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 @@ -0,0 +1,22 @@ +using module Microsoft.Windows.Setting.Language + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +<# +.Synopsis + Pester tests related to the Microsoft.Windows.Settings.Language PowerShell module. +#> + +BeforeAll { + Import-Module Microsoft.Windows.Settings.Language -Force -ErrorAction SilentlyContinue +} + +Describe 'List available DSC resources' { + It 'Shows DSC Resources' { + $expectedDSCResources = "Language" + $availableDSCResources = (Get-DscResource -Module Microsoft.Windows.Settings.Language).Name + $availableDSCResources.count | Should -Be 1 + $availableDSCResources | Where-Object { $expectedDSCResources -notcontains $_ } | Should -BeNullOrEmpty -ErrorAction Stop + } +} diff --git a/utilities/scripts/New-DscResourceModule.ps1 b/utilities/scripts/New-DscResourceModule.ps1 new file mode 100644 index 00000000..14f5a89f --- /dev/null +++ b/utilities/scripts/New-DscResourceModule.ps1 @@ -0,0 +1,98 @@ +function New-DscResourceModule +{ + <# + .SYNOPSIS + Creates a new DSC (Desired State Configuration) resource module structure. + + .DESCRIPTION + The function New-DscResourceModule function creates a new DSC resource module structure with the specified name and description. + It sets up the necessary directory structure for resources and tests within the given base path. + + .PARAMETER DscResourceModule + The name of the DSC resource module to create. + + .PARAMETER Description + A description of the DSC resource module. + + .PARAMETER BasePath + The base path where the DSC resource module structure will be created. The default value is the parent directory of the script. + + .EXAMPLE + PS C:\> New-DscResourceModule -DscResourceModule 'Microsoft.Windows.Language' -Description 'DSC Resource for Windows Language' + + This command creates a new DSC resource module named 'Microsoft.Windows.Language' with the specified description in the default base path. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string]$DscResourceModule, + + [Parameter(Mandatory)] + [string]$Description, + + [Parameter()] + [string]$BasePath = (Join-Path $PSScriptRoot '..' '..') + + ) + + $resourcePath = Join-Path $BasePath 'resources' $DscResourceModule + $testsPath = Join-Path $BasePath 'tests' $DscResourceModule + + # Create directories if they do not exist + if (-not (Test-Path -Path $resourcePath)) + { + Write-Verbose -Message "Creating directory: $resourcePath" + $null = New-Item -ItemType Directory -Path $resourcePath -Force + } + + if (-not (Test-Path -Path $testsPath)) + { + Write-Verbose -Message "Creating test directory: $testsPath" + $null = New-Item -ItemType Directory -Path $testsPath -Force + } + + $moduleManifestPath = (Join-Path $BasePath 'resources' $DscResourceModule "$DscResourceModule.psd1") + + $moduleManifestParams = @{ + Path = $moduleManifestPath + RootModule = "$DscResourceModule.psm1" + ModuleVersion = '0.1.0' + Author = 'Microsoft Corporation' + CompanyName = 'Microsoft Corporation' + Copyright = '(c) Microsoft Corporation. All rights reserved.' + Description = $Description + PowerShellVersion = '7.2' + DscResourcesToExport = @() + } + + if (-not (Test-Path $moduleManifestPath)) + { + Write-Verbose -Message ("Creating module manifest in: $moduleManifestPath with") + Write-Verbose -Message ($moduleManifestParams | ConvertTo-Json -Depth 10 | Out-String) + New-ModuleManifest @moduleManifestParams + + # Workaround for issue: https://github.com/PowerShell/PowerShell/issues/5922 + $fileContent = Get-Content $moduleManifestPath + $newLicenseUri = "LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE'" + $fileContent = $fileContent -replace '# LicenseUri = ''''', $newLicenseUri + $newProjectUri = "ProjectUri = 'https://github.com/microsoft/winget-dsc'" + $fileContent = $fileContent -replace '# ProjectUri = ''''', $newProjectUri + $newPrerelease = "Prerelease = 'alpha'" + $fileContent = $fileContent -replace '# Prerelease = ''''', $newPrerelease + # TODO: Add tags + + Set-Content -Path $moduleManifestPath -Value $fileContent + } + + $psm1Path = Join-Path -Path $resourcePath -ChildPath "$DscResourceModule.psm1" + if (-not (Test-Path $psm1Path)) + { + $null = New-Item -ItemType File -Path $psm1Path -Force + } + + $testsFilePath = Join-Path -Path $testsPath -ChildPath "$DscResourceModule.Tests.ps1" + if (-not (Test-Path $testsFilePath)) + { + $null = New-Item -ItemType File -Path $testsFilePath -Force + } +} \ No newline at end of file From 51c2b630d9a33e247b8f0affcb6377b3df8a9945 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 07:23:38 +0100 Subject: [PATCH 03/33] Add examples --- pipelines/azure-pipelines.yml | 4 + .../Microsoft.VSCode.Dsc/VSCodeExtension.md | 2 +- .../DisplayLanguage.md | 36 ++++ .../Language.md | 36 ++++ .../Microsoft.Windows.Setting.Language.psd1 | 161 +++++++++--------- .../Microsoft.Windows.Setting.Language.psm1 | 160 ++++++++++++++++- ...crosoft.Windows.Setting.Language.Tests.ps1 | 49 +++++- 7 files changed, 358 insertions(+), 90 deletions(-) create mode 100644 resources/Help/Microsoft.Windows.Setting.Language/DisplayLanguage.md create mode 100644 resources/Help/Microsoft.Windows.Setting.Language/Language.md diff --git a/pipelines/azure-pipelines.yml b/pipelines/azure-pipelines.yml index 8e619f41..cb3fa48b 100644 --- a/pipelines/azure-pipelines.yml +++ b/pipelines/azure-pipelines.yml @@ -52,6 +52,10 @@ extends: displayName: "Publish Pipeline Microsoft.Windows.Setting.Accessibility" targetPath: $(Build.SourcesDirectory)\resources\Microsoft.Windows.Setting.Accessibility\ artifactName: Microsoft.Windows.Setting.Accessibility + - output: pipelineArtifact + displayName: "Publish Pipeline Microsoft.Windows.Setting.Language" + targetPath: $(Build.SourcesDirectory)\resources\Microsoft.Windows.Setting.Language\ + artifactName: Microsoft.Windows.Setting.Language - output: pipelineArtifact displayName: "Publish Pipeline PythonPip3Dsc" targetPath: $(Build.SourcesDirectory)\resources\PythonPip3Dsc\ diff --git a/resources/Help/Microsoft.VSCode.Dsc/VSCodeExtension.md b/resources/Help/Microsoft.VSCode.Dsc/VSCodeExtension.md index 4681e7a6..925053e4 100644 --- a/resources/Help/Microsoft.VSCode.Dsc/VSCodeExtension.md +++ b/resources/Help/Microsoft.VSCode.Dsc/VSCodeExtension.md @@ -28,7 +28,7 @@ The `VSCodeExtension` DSC Resource allows you to install, update, and remove Vis ## EXAMPLES -### Example 1 +### EXAMPLE 1 ```powershell # Install the latest version of the Visual Studio Code extension 'ms-python.python' diff --git a/resources/Help/Microsoft.Windows.Setting.Language/DisplayLanguage.md b/resources/Help/Microsoft.Windows.Setting.Language/DisplayLanguage.md new file mode 100644 index 00000000..100d8cd9 --- /dev/null +++ b/resources/Help/Microsoft.Windows.Setting.Language/DisplayLanguage.md @@ -0,0 +1,36 @@ +--- +external help file: Microsoft.Windows.Setting.Language.psm1-Help.xml +Module Name: Microsoft.Windows.Setting.Language +ms.date: 11/04/2024 +online version: +schema: 2.0.0 +title: DisplayLanguage +--- + +# DisplayLanguage + +## SYNOPSIS + +The `DisplayLanguage` DSC Resource allows you to set the display language on your local Windows machine. + +## DESCRIPTION + +The `DisplayLanguage` DSC Resource allows you to set the display language on your local Windows machine. + +## PARAMETERS + +| **Parameter** | **Attribute** | **DataType** | **Description** | **Allowed Values** | +| ------------- | ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| `LocaleName` | Mandatory | String | The name of the language. This is the language tag that represents the language. For example, `en-US` represents English (United States). | Use the `Get-WinUserLanguageList` to see what language pack have been installed. | +| `Exist` | Optional | Boolean | Indicates whether the extension should exist. The default value is `$true`. | `$true`, `$false` | + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +$params = @{ + LocaleName = 'en-US' +} +Invoke-DscResource -Name DisplayLanguage -Method Set -Property $params -ModuleName Microsoft.Windows.Setting.Language +``` diff --git a/resources/Help/Microsoft.Windows.Setting.Language/Language.md b/resources/Help/Microsoft.Windows.Setting.Language/Language.md new file mode 100644 index 00000000..ccea48a1 --- /dev/null +++ b/resources/Help/Microsoft.Windows.Setting.Language/Language.md @@ -0,0 +1,36 @@ +--- +external help file: Microsoft.Windows.Setting.Language.psm1-Help.xml +Module Name: Microsoft.Windows.Setting.Language +ms.date: 11/04/2024 +online version: +schema: 2.0.0 +title: Language +--- + +# Language + +## SYNOPSIS + +The `Language` DSC Resource allows you to install, update, and uninstall languages on your local Windows machine. + +## DESCRIPTION + +The `Language` DSC Resource allows you to install, update, and uninstall languages on your local Windows machine. + +## PARAMETERS + +| **Parameter** | **Attribute** | **DataType** | **Description** | **Allowed Values** | +| ------------- | ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `LocaleName` | Mandatory | String | The name of the language. This is the language tag that represents the language. For example, `en-US` represents English (United States). | Use the `Get-LocaleList` function or Export() method to get a list of allowed values. | +| `Exist` | Optional | Boolean | Indicates whether the extension should exist. The default value is `$true`. | `$true`, `$false` | + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +$params = @{ + LocaleName = 'en-US' +} +Invoke-DscResource -Name Language -Method Set -Property $params -ModuleName Microsoft.Windows.Setting.Language +``` diff --git a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psd1 b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psd1 index e98c3d4f..2e758bbf 100644 --- a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psd1 +++ b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psd1 @@ -8,125 +8,128 @@ @{ -# Script module or binary module file associated with this manifest. -RootModule = 'Microsoft.Windows.Setting.Language.psm1' + # Script module or binary module file associated with this manifest. + RootModule = 'Microsoft.Windows.Setting.Language.psm1' -# Version number of this module. -ModuleVersion = '0.1.0' + # Version number of this module. + ModuleVersion = '0.1.0' -# Supported PSEditions -# CompatiblePSEditions = @() + # Supported PSEditions + # CompatiblePSEditions = @() -# ID used to uniquely identify this module -GUID = '6ab8bbf6-ce28-4d33-a3ce-04c1cc16f139' + # ID used to uniquely identify this module + GUID = '6ab8bbf6-ce28-4d33-a3ce-04c1cc16f139' -# Author of this module -Author = 'Microsoft Corporation' + # Author of this module + Author = 'Microsoft Corporation' -# Company or vendor of this module -CompanyName = 'Microsoft Corporation' + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' -# Copyright statement for this module -Copyright = '(c) Microsoft Corporation. All rights reserved.' + # Copyright statement for this module + Copyright = '(c) Microsoft Corporation. All rights reserved.' -# Description of the functionality provided by this module -Description = 'DSC Resource for Windows Setting Language' + # Description of the functionality provided by this module + Description = 'DSC Resource for Windows Setting Language' -# Minimum version of the PowerShell engine required by this module -PowerShellVersion = '7.2' + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '7.2' -# Name of the PowerShell host required by this module -# PowerShellHostName = '' + # Name of the PowerShell host required by this module + # PowerShellHostName = '' -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# ClrVersion = '' + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = '*' + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = '*' -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = '*' + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = '*' -# Variables to export from this module -VariablesToExport = '*' + # Variables to export from this module + VariablesToExport = '*' -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '*' + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = '*' -# DSC resources to export from this module -# DscResourcesToExport = @() + # DSC resources to export from this module + DscResourcesToExport = @("Language", "DisplayLanguage") -# List of all modules packaged with this module -# ModuleList = @() + # List of all modules packaged with this module + # ModuleList = @() -# List of all files packaged with this module -# FileList = @() + # List of all files packaged with this module + # FileList = @() -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ - PSData = @{ + PSData = @{ - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'PSDscResource_Language', + 'PSDscResource_DisplayLanguage' + ) - # A URL to the license for this module. - LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' - # A URL to the main website for this project. - ProjectUri = 'https://github.com/microsoft/winget-dsc' + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-dsc' - # A URL to an icon representing this module. - # IconUri = '' + # A URL to an icon representing this module. + # IconUri = '' - # ReleaseNotes of this module - # ReleaseNotes = '' + # ReleaseNotes of this module + # ReleaseNotes = '' - # Prerelease string of this module - Prerelease = 'alpha' + # Prerelease string of this module + Prerelease = 'alpha' - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false - # External dependent modules of this module - # ExternalModuleDependencies = @() + # External dependent modules of this module + # ExternalModuleDependencies = @() - } # End of PSData hashtable + } # End of PSData hashtable -} # End of PrivateData hashtable + } # End of PrivateData hashtable -# HelpInfo URI of this module -# HelpInfoURI = '' + # HelpInfo URI of this module + # HelpInfoURI = '' -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' } diff --git a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 index b8c26da5..e9bbddc8 100644 --- a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 +++ b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 @@ -1,3 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +using namespace System.Collections.Generic + $global:LocaleNameRegistryPath = "HKCU:\Control Panel\International" $global:LocaleUserProfilePath = "HKCU:\Control Panel\International\User Profile" @@ -90,11 +95,156 @@ function TryGetRegistryValue Write-Verbose "Registry key does not exist." } } + +function Get-LocaleList +{ + # TODO: Add support for older OS versions + $localeList = Get-WinUserLanguageList + $out = [List[Language]]::new() + + foreach ($locale in $localeList) + { + $langague = [Language]::new($locale.LanguageTag, $true) + $out.Add($langague) + } + + # section to include other languages that can be installed + # helpful for users to discover what packages can be installed + $allLangues = [System.Globalization.CultureInfo]::GetCultures("AllCultures") + foreach ($culture in $allLangues) + { + if ($out.LocaleName -notcontains $culture.Name -and -not ([string]::IsNullOrEmpty($culture.Name))) + { + $langague = [Language]::new($culture.Name, $false) + $out.Add($langague) + } + } + + return $out +} #endregion Functions #region Classes +<# +.SYNOPSIS + The `Language` DSC Resource allows you to install, update, and uninstall languages on your local Windows machine. + +.PARAMETER LocaleName + The name of the language. This is the language tag that represents the language. For example, `en-US` represents English (United States). + To get a full list of languages available, use the `Get-LocaleList` function or Export() method. + +.PARAMETER Exist + Indicates whether the package should exist. Defaults to $true. + +.EXAMPLE + PS C:\> Invoke-DscResource -ModuleName Microsoft.Windows.Setting.Language -Name Language -Method Set -Property @{ LocaleName = 'en-US' } + + This example installs the English (United States) language on the local machine. +#> +[DscResource()] +class Language +{ + [DscProperty(Key)] + [string] $LocaleName + + [DscProperty()] + [bool] $Exist = $true + + static [hashtable] $InstalledLocality + + Language() + { + [Language]::GetInstalledLocality() + } + + Language([string] $LocaleName, [bool] $Exist) + { + $this.LocaleName = $LocaleName + $this.Exist = $Exist + } + + [Language] Get() + { + $keyExist = [Language]::InstalledLocality.ContainsKey(($this.LocaleName)) + + $currentState = [Language]::InstalledLocality[$this.LocaleName] + + if (-not $keyExist) + { + return [Language]::new($this.LocaleName, $false) + } + + return $currentState + } + + [void] Set() + { + if ($this.Test()) + { + return + } + + if (Test-Win11OrServer2022) + { + if ($this.Exist) + { + # use the LanguagePackManagement module to install the language (requires elevation). International does not have a cmdlet to install language + Install-Language -Language $this.LocaleName + } + else + { + Uninstall-Language -Language $this.LocaleName + } + } + } + + [bool] Test() + { + $currentState = $this.Get() + + if ($currentState.Exist -ne $this.Exist) + { + return $false + } + + return $true + } + + static [Language[]] Export() + { + return Get-LocaleList + } + + #region Language helper functions + static [void] GetInstalledLocality() + { + [Language]::InstalledLocality = @{} + + foreach ($locality in [Language]::Export()) + { + [Language]::InstalledLocality[$locality.LocaleName] = $locality + } + } + #endRegion Language helper functions +} + +<# +.SYNOPSIS + The `DisplayLanguage` DSC Resource allows you to set the display language on your local Windows machine. + +.PARAMETER LocaleName + The name of the display language. This is the language tag that represents the language. For example, `en-US` represents English (United States). + +.PARAMETER Exist + Indicates whether the display language should be set. Defaults to $true. + +.EXAMPLE + PS C:\> Invoke-DscResource -ModuleName Microsoft.Windows.Setting.Language -Name DisplayLanguage -Method Set -Property @{ LocaleName = 'en-US' } + + This example sets the display language to English (United States) on the user. +#> [DscResource()] -class Language +class DisplayLanguage { [DscProperty(Key)] @@ -105,16 +255,16 @@ class Language hidden [string] $KeyName = 'LocaleName' - Language() + DisplayLanguage() { $this } - [Language] Get() + [DisplayLanguage] Get() { - $currentState = [Language]::new() + $currentState = [DisplayLanguage]::new() - # check if user profile contains language + # check if user profile contains display language $userProfileLanguageDict = TryGetRegistryValue -Key (Join-path $global:LocaleUserProfilePath $this.LocaleName) -Property 'CachedLanguageName' if ((TryGetRegistryValue -Key $global:LocaleNameRegistryPath -Property $this.KeyName) -ne $this.LocaleName -and ($null -ne $userProfileLanguageDict)) { diff --git a/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 b/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 index 2e819b19..9ec7f0dd 100644 --- a/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 +++ b/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 @@ -5,18 +5,57 @@ Set-StrictMode -Version Latest <# .Synopsis - Pester tests related to the Microsoft.Windows.Settings.Language PowerShell module. + Pester tests related to the Microsoft.Windows.Setting.Language PowerShell module. #> BeforeAll { - Import-Module Microsoft.Windows.Settings.Language -Force -ErrorAction SilentlyContinue + Import-Module Microsoft.Windows.Setting.Language -Force -ErrorAction SilentlyContinue } Describe 'List available DSC resources' { It 'Shows DSC Resources' { - $expectedDSCResources = "Language" - $availableDSCResources = (Get-DscResource -Module Microsoft.Windows.Settings.Language).Name - $availableDSCResources.count | Should -Be 1 + $expectedDSCResources = @("Language", "DisplayLanguage") + $availableDSCResources = (Get-DscResource -Module Microsoft.Windows.Setting.Language).Name + $availableDSCResources.count | Should -Be 2 $availableDSCResources | Where-Object { $expectedDSCResources -notcontains $_ } | Should -BeNullOrEmpty -ErrorAction Stop } } + +Describe 'Language' { + It 'Install a preferred language' -Skip:(!$IsWindows) { + $desiredState = @{ + LocaleName = 'en-GB' + } + + Invoke-DscResource -Name Language -ModuleName Microsoft.Windows.Setting.Language -Method Set -Property $desiredState + + $finalState = Invoke-DscResource -Name Language -ModuleName Microsoft.Windows.Setting.Language -Method Get -Property $desiredState + $finalState.Exist | Should -BeTrue + } + + It 'Uninstall a preferred language' -Skip:(!$IsWindows) { + $desiredState = @{ + LocaleName = 'en-GB' + } + + Invoke-DscResource -Name Pip3Package -ModuleName PythonPip3Dsc -Method Set -Property $desiredState + + $finalState = Invoke-DscResource -Name Pip3Package -ModuleName PythonPip3Dsc -Method Get -Property $desiredState + $finalState.Exist | Should -BeFalse + } + + # TODO: Add test if LocaleName is not found +} + +Describe 'DisplayLanguage' { + It 'Set a preferred language' -Skip:(!$IsWindows) { + $desiredState = @{ + LocaleName = 'en-US' + } + + Invoke-DscResource -Name DisplayLanguage -ModuleName Microsoft.Windows.Setting.Language -Method Set -Property $desiredState + + $finalState = Invoke-DscResource -Name Language -ModuleName Microsoft.Windows.Setting.Language -Method Get -Property $desiredState + $finalState.Exist | Should -BeTrue + } +} \ No newline at end of file From 83eb7a0bf0b4c352d68a2b58d82a36f85c4bca22 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 07:30:39 +0100 Subject: [PATCH 04/33] Update command to directly call language installer --- .../Microsoft.Windows.Setting.Language.psm1 | 77 ++----------------- 1 file changed, 8 insertions(+), 69 deletions(-) diff --git a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 index e9bbddc8..cc1e25f5 100644 --- a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 +++ b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 @@ -12,63 +12,6 @@ function Get-OsBuildVersion return [System.Environment]::OSVersion.Version.Build } -function Set-LocaleByOs -{ - param ( - [Parameter(Mandatory = $true)] - [string]$LocaleName - ) - - if (Test-Win11OrServer2022) - { - if (Test-LocaleByOs -LocaleName $LocaleName) - { - Set-WinUserLanguageList -Language $LocaleName - } - } - # TODO: Add support for older OS versions - # Challenging to get input method tips for older OS versions - else - { - Throw "This module only supports Windows 11 and Windows Server 2022." - } -} - -function Test-Win11OrServer2022 -{ - $osBuildVersion = Get-OsBuildVersion - - if ($osBuildVersion -gt 26100 -or $osBuildVersion -gt 20348) - { - return $true - } - - return $false -} - -function Test-LocaleByOs -{ - param ( - [Parameter(Mandatory = $true)] - [string]$LocaleName - ) - - $osBuildVersion = Get-OsBuildVersion - - if ($osBuildVersion -gt 26100 -or $osBuildVersion -gt 20348) - { - $languageList = Get-WinUserLanguageList - if ($languageList.Language -in $LocaleName) - { - return $true - } - else - { - Throw "Language `"$($LocaleName)`" is not installed. Please make sure the language is installed on the system first." - } - } -} - function TryGetRegistryValue { param ( @@ -184,17 +127,14 @@ class Language return } - if (Test-Win11OrServer2022) + if ($this.Exist) + { + # use the LanguagePackManagement module to install the language (requires elevation). International does not have a cmdlet to install language + Install-Language -Language $this.LocaleName + } + else { - if ($this.Exist) - { - # use the LanguagePackManagement module to install the language (requires elevation). International does not have a cmdlet to install language - Install-Language -Language $this.LocaleName - } - else - { - Uninstall-Language -Language $this.LocaleName - } + Uninstall-Language -Language $this.LocaleName } } @@ -257,7 +197,6 @@ class DisplayLanguage DisplayLanguage() { - $this } [DisplayLanguage] Get() @@ -286,7 +225,7 @@ class DisplayLanguage } # TODO: How do we handle sign out and sign in? - Set-LocaleByOs -LocaleName $this.LocaleName + Set-WinUserLanguageList -Language $this.LocaleName # TODO: Exist does not make sense here, we always want a language to exist } From 55b01a53675a5e4cf46e743ddabb2fe8e66114f4 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 07:38:03 +0100 Subject: [PATCH 05/33] Update contribution --- CONTRIBUTING.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75c153fa..6ae7e253 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,14 +109,28 @@ Once the team have approved an issue/spec, development can proceed. If no develo ### Fork, Clone, Branch and Create your PR -Once you've discussed your proposed feature/fix/etc. with a team member, and you've agreed an approach or a spec has been written and approved, it's time to start development: +Once you've discussed your proposed feature/fix/etc. with a team member, and you've agreed an approach or a spec has been written and approved, it's time to start development. There are two flows you can follow depending on the proposed feature. + +If you're feature (or module) has not yet been created, follow these steps: + +1. Fork the repository if you haven't already. +2. Clone your fork locally. +3. Dot-source the `New-DscResourceModule.ps` in your PowerShell session. +4. Create a new module scaffolding by executing: `New-DscResourceModule -DscResourceModule '' -Description 'DSC Resource for '` +5. Work on your changes and write tests. +6. Build and test to see if it works. +7. Create & push a feature branch. +8. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/). +9. If you are finished with your changes and you want a review, change the state. + +When you are working on a fix, you can follow the below steps: 1. Fork the repository if you haven't already. -1. Clone your fork locally. -1. Create & push a feature branch. -1. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/). -1. Work on your changes. -1. Build and see if it works. +2. Clone your fork locally. +3. Work on your fix and _optionally_ write tests +4. Build and test to see if it works. +5. Create & push a feature branch. +6. Create a [Pull Request (PR)](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) when you are finished with your changes ### Testing @@ -126,7 +140,8 @@ Testing is a key component in the development workflow. When you'd like the team to take a look, (even if the work is not yet fully-complete), mark the Draft PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge. -> ⚠ Remember: **changes you make may affect both the Windows Package Manager and the schema support implemented in our validation pipelines!** Because of this, we will treat community PR's with the same level of scrutiny and rigor as commits submitted to the official Windows source by team members and partners. +> [!CAUTION] +> Remember: **changes you make may affect both the Windows Package Manager and the schema support implemented in our validation pipelines!** Because of this, we will treat community PR's with the same level of scrutiny and rigor as commits submitted to the official Windows source by team members and partners. ### Merge From c3847d29b7bae217787b22a959962574c0b3e1bd Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 07:39:33 +0100 Subject: [PATCH 06/33] Clarification --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ae7e253..45694a1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,7 +109,7 @@ Once the team have approved an issue/spec, development can proceed. If no develo ### Fork, Clone, Branch and Create your PR -Once you've discussed your proposed feature/fix/etc. with a team member, and you've agreed an approach or a spec has been written and approved, it's time to start development. There are two flows you can follow depending on the proposed feature. +Once you've discussed your proposed feature/fix/etc. with a team member, and you've agreed an approach or a spec has been written and approved, it's time to start development. There are two flows you can follow depending on the proposed feature or fix. If you're feature (or module) has not yet been created, follow these steps: @@ -123,11 +123,11 @@ If you're feature (or module) has not yet been created, follow these steps: 8. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/). 9. If you are finished with your changes and you want a review, change the state. -When you are working on a fix, you can follow the below steps: +When you are working on a fix or you want to add additional features to an existing module, you can follow the below steps: 1. Fork the repository if you haven't already. 2. Clone your fork locally. -3. Work on your fix and _optionally_ write tests +3. Work on your fix or feature, and _optionally_ write tests 4. Build and test to see if it works. 5. Create & push a feature branch. 6. Create a [Pull Request (PR)](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) when you are finished with your changes From 251d306a90ed184fcf29b9eb1e2fb119a9cd6e7a Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 07:43:20 +0100 Subject: [PATCH 07/33] Export test --- .../Microsoft.Windows.Setting.Language.Tests.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 b/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 index 9ec7f0dd..8565fbfc 100644 --- a/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 +++ b/tests/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.Tests.ps1 @@ -44,6 +44,15 @@ Describe 'Language' { $finalState.Exist | Should -BeFalse } + It 'Export all languages' -Skip:(!$IsWindows) { + + $class = [Language]::new() + + $currentLanguages = $class::Export() + $currentLanguages | Should -Not -BeNullOrEmpty + $currentLanguages.Count | Should -BeGreaterThan 0 + } + # TODO: Add test if LocaleName is not found } From 0f14f9d4114e340d90ab83ef40d74ad2898f9bbf Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 07:44:02 +0100 Subject: [PATCH 08/33] Fix TODO --- .../Microsoft.Windows.Setting.Language.psm1 | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 index cc1e25f5..1e5652b2 100644 --- a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 +++ b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 @@ -41,7 +41,6 @@ function TryGetRegistryValue function Get-LocaleList { - # TODO: Add support for older OS versions $localeList = Get-WinUserLanguageList $out = [List[Language]]::new() From b8dd560bfa22008fd878909efce6e1fd36208220 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 08:56:11 +0100 Subject: [PATCH 09/33] Additional tip --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45694a1e..2faf1298 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,6 +123,9 @@ If you're feature (or module) has not yet been created, follow these steps: 8. Create a [Draft Pull Request (PR)](https://github.blog/2019-02-14-introducing-draft-pull-requests/). 9. If you are finished with your changes and you want a review, change the state. +> [!TIP] +> Don't forget to add the `DscResourcesToExport` and `Tags`. + When you are working on a fix or you want to add additional features to an existing module, you can follow the below steps: 1. Fork the repository if you haven't already. From 323d6246cb509505f092ea3433fad7bc33435dab Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 10:40:47 +0100 Subject: [PATCH 10/33] Initial module for Windows Update settings --- ...crosoft.Windows.Setting.WindowsUpdate.psd1 | 134 +++++++++ ...crosoft.Windows.Setting.WindowsUpdate.psm1 | 284 ++++++++++++++++++ ...ft.Windows.Setting.WindowsUpdate.Tests.ps1 | 24 ++ 3 files changed, 442 insertions(+) create mode 100644 resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psd1 create mode 100644 resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 create mode 100644 tests/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.Tests.ps1 diff --git a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psd1 b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psd1 new file mode 100644 index 00000000..7636d788 --- /dev/null +++ b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psd1 @@ -0,0 +1,134 @@ +# +# Module manifest for module 'Microsoft.Windows.Setting.WindowsUpdate' +# +# Generated by: Microsoft Corporation +# +# Generated on: 04/11/2024 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'Microsoft.Windows.Setting.WindowsUpdate.psm1' + + # Version number of this module. + ModuleVersion = '0.1.0' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = '6a0a9e72-9797-4c28-94ca-ebfbef3d7116' + + # Author of this module + Author = 'Microsoft Corporation' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = '(c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'DSC Resource for Windows Update Settings' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '7.2' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = '*' + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = '*' + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = '*' + + # DSC resources to export from this module + DscResourcesToExport = @('WindowsUpdate') + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'PSDscResource_WindowsUpdate' + ) + + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-dsc' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + Prerelease = 'alpha' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + +} + diff --git a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 new file mode 100644 index 00000000..20eccd63 --- /dev/null +++ b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 @@ -0,0 +1,284 @@ +$global:WindowsUpdateSettingPath = 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' + +#region Functions +function DoesRegistryKeyPropertyExist +{ + param ( + [Parameter(Mandatory)] + [string]$Path, + + [Parameter(Mandatory)] + [string]$Name + ) + + # Get-ItemProperty will return $null if the registry key property does not exist. + $itemProperty = Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue + return $null -ne $itemProperty +} + +function Test-WindowsUpdateRegistryKey +{ + param ( + [Parameter(Mandatory)] + [hashtable] $RegistryKeyProperty, + + [Parameter(Mandatory)] + [WindowsUpdate]$CurrentState + ) + + $result = $true + foreach ($key in $RegistryKeyProperty.Keys) + { + $value = $RegistryKeyProperty[$key] + if ($value -ne $CurrentState.$key) + { + $result = $false + } + } + + return $result +} + +function Set-WindowsUpdateRegistryKey +{ + param ( + [Parameter(Mandatory)] + [string]$Path, + + [Parameter()] + [AllowNull()] + [hashtable] $RegistryKeyProperty + ) + + if (-not (Test-Path -Path $Path)) + { + $null = New-Item -Path $Path -Force + } + + foreach ($key in $RegistryKeyProperty.Keys) + { + $value = $RegistryKeyProperty[$key] + $typeInfo = $value.GetType().Name + + if ($typeInfo -eq 'Boolean') + { + $value = [int]$value + } + + if ($typeInfo -eq 'Int32' -and $key -in @('UserChoiceActiveHoursEnd', 'UserChoiceActiveHoursStart')) + { + if ($value -notin (0..24)) + { + Throw "Value for $key must be between 0 and 24" + } + } + + if (-not (DoesRegistryKeyPropertyExist -Path $Path -Name $key)) + { + $null = New-ItemProperty -Path $Path -Name $key -Value $value -PropertyType 'DWord' -Force + } + + Write-Verbose -Message "Setting $key to $($RegistryKeyProperty[$key])" + Set-ItemProperty -Path $Path -Name $key -Value $value + } +} +#endregion Functions + +#region Classes +[DSCResource()] +class WindowsUpdate +{ + # Key required. Do not set. + [DscProperty(Key)] + [string] $SID + + [DscProperty()] + [nullable[bool]] $IsContinuousInnovationOptedIn + + [DscProperty()] + [nullable[bool]] $AllowMUUpdateService + + [DscProperty()] + [nullable[bool]] $IsExpedited + + [DscProperty()] + [nullable[bool]] $AllowAutoWindowsUpdateDownloadOverMeteredNetwork + + [DscProperty()] + [nullable[bool]] $RestartNotificationsAllowed + + [DscProperty()] + [nullable[bool]] $SmartActiveHoursState + + [DscProperty()] + [nullable[int]] $UserChoiceActiveHoursEnd + + [DscProperty()] + [nullable[int]] $UserChoiceActiveHoursStart + + static hidden [string] $IsContinuousInnovationOptedInProperty = 'IsContinuousInnovationOptedIn' + static hidden [string] $AllowMUUpdateServiceProperty = 'AllowMUUpdateService' + static hidden [string] $IsExpeditedProperty = 'IsExpedited' + static hidden [string] $AllowAutoWindowsUpdateDownloadOverMeteredNetworkProperty = 'AllowAutoWindowsUpdateDownloadOverMeteredNetwork' + static hidden [string] $RestartNotificationsAllowedProperty = 'RestartNotificationsAllowed2' + static hidden [string] $SmartActiveHoursStateProperty = 'SmartActiveHoursState' + static hidden [string] $UserChoiceActiveHoursEndProperty = 'UserChoiceActiveHoursEnd' + static hidden [string] $UserChoiceActiveHoursStartProperty = 'UserChoiceActiveHoursStart' + + [WindowsUpdate] Get() + { + $currentState = [WindowsUpdate]::new() + $currentState.IsContinuousInnovationOptedIn = [WindowsUpdate]::GetIsContinuousInnovationOptedInStatus() + $currentState.AllowMUUpdateService = [WindowsUpdate]::AllowMUUpdateServiceStatus() + $currentState.IsExpedited = [WindowsUpdate]::IsExpeditedStatus() + $currentState.AllowAutoWindowsUpdateDownloadOverMeteredNetwork = [WindowsUpdate]::AllowAutoWindowsUpdateDownloadOverMeteredNetworkStatus() + $currentState.RestartNotificationsAllowed = [WindowsUpdate]::RestartNotificationsAllowedStatus() + $currentState.SmartActiveHoursState = [WindowsUpdate]::SmartActiveHoursStateStatus() + $currentState.UserChoiceActiveHoursEnd = [WindowsUpdate]::UserChoiceActiveHoursEndStatus() + $currentState.UserChoiceActiveHoursStart = [WindowsUpdate]::UserChoiceActiveHoursStartStatus() + + return $currentState + } + + [bool] Test() + { + $currentState = $this.Get() + $settableProperties = $this.GetParameters() + return (Test-WindowsUpdateRegistryKey -RegistryKeyProperty $settableProperties -CurrentState $currentState) + } + + [void] Set() + { + if ($this.Test()) + { + return + } + + $parameters = $this.GetParameters() + + Set-WindowsUpdateRegistryKey -Path $global:WindowsUpdateSettingPath -RegistryKeyProperty $parameters + } + + #region WindowsUpdate helper functions + static [bool] GetIsContinuousInnovationOptedInStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::IsContinuousInnovationOptedInProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::IsContinuousInnovationOptedInProperty).IsContinuousInnovationOptedInProperty + return $value + } + } + + static [bool] AllowMUUpdateServiceStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::AllowMUUpdateServiceProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::AllowMUUpdateServiceProperty).AllowMUUpdateServiceProperty + return $value + } + } + + static [bool] IsExpeditedStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::IsExpeditedProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::IsExpeditedProperty).IsExpeditedProperty + return $value + } + } + + static [bool] AllowAutoWindowsUpdateDownloadOverMeteredNetworkStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::AllowAutoWindowsUpdateDownloadOverMeteredNetworkProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::AllowAutoWindowsUpdateDownloadOverMeteredNetworkProperty).AllowAutoWindowsUpdateDownloadOverMeteredNetworkProperty + return $value + } + } + + static [bool] RestartNotificationsAllowedStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::RestartNotificationsAllowedProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::RestartNotificationsAllowedProperty).RestartNotificationsAllowed + return $value + } + } + + static [bool] SmartActiveHoursStateStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::SmartActiveHoursStateProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::SmartActiveHoursStateProperty).SmartActiveHoursState + return $value + } + } + + static [int] UserChoiceActiveHoursEndStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::UserChoiceActiveHoursEndProperty))) + { + return $false + } + else + { + # there is some weird behaviour with integers in the registry, so we need to get the value from the property + $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::UserChoiceActiveHoursEndProperty) | Select-Object -ExpandProperty UserChoiceActiveHoursEnd + + return $value + } + } + + static [int] UserChoiceActiveHoursStartStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::UserChoiceActiveHoursStartProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::UserChoiceActiveHoursStartProperty) | Select-Object -ExpandProperty UserChoiceActiveHoursStart + return $value + } + } + + [hashtable] GetParameters() + { + $parameters = @{} + foreach ($property in $this.PSObject.Properties) + { + if (-not ([string]::IsNullOrEmpty($property.Value))) + { + $parameters[$property.Name] = $property.Value + } + } + + return $parameters + } + #endRegion WindowsUpdate helper functions +} +#endRegion classes \ No newline at end of file diff --git a/tests/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.Tests.ps1 b/tests/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.Tests.ps1 new file mode 100644 index 00000000..229d2bce --- /dev/null +++ b/tests/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.Tests.ps1 @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +using module Microsoft.Windows.Setting.WindowsUpdate + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +<# +.Synopsis + Pester tests related to the Microsoft.Windows.Setting.WindowsUpdate PowerShell module. +#> + +BeforeAll { + Import-Module Microsoft.Windows.Setting.WindowsUpdate +} + +Describe 'List available DSC resources' { + It 'Shows DSC Resources' { + $expectedDSCResources = "WindowsUpdate" + $availableDSCResources = (Get-DscResource -Module Microsoft.Windows.Setting.WindowsUpdate).Name + $availableDSCResources.count | Should -Be 1 + $availableDSCResources | Where-Object { $expectedDSCResources -notcontains $_ } | Should -BeNullOrEmpty -ErrorAction Stop + } +} \ No newline at end of file From 0ba034a4846e39d6c6d48b5827c7e8bf5a509846 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 12:03:53 +0100 Subject: [PATCH 11/33] TODO task --- .../Microsoft.Windows.Setting.WindowsUpdate.psm1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 index 20eccd63..a2d62a92 100644 --- a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 +++ b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 @@ -116,6 +116,8 @@ class WindowsUpdate [DscProperty()] [nullable[int]] $UserChoiceActiveHoursStart + # TODO: Add delivery options + static hidden [string] $IsContinuousInnovationOptedInProperty = 'IsContinuousInnovationOptedIn' static hidden [string] $AllowMUUpdateServiceProperty = 'AllowMUUpdateService' static hidden [string] $IsExpeditedProperty = 'IsExpedited' From 7ba6a698880b90d21f43834c6cb924b3e35a858f Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 12:58:38 +0100 Subject: [PATCH 12/33] Add assertions for delivery optimization --- ...crosoft.Windows.Setting.WindowsUpdate.psm1 | 210 +++++++++++++++++- 1 file changed, 202 insertions(+), 8 deletions(-) diff --git a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 index a2d62a92..bd60c883 100644 --- a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 +++ b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 @@ -1,4 +1,5 @@ $global:WindowsUpdateSettingPath = 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' +$global:DeliveryOptimizationSettingPath = 'Registry::HKEY_USERS\S-1-5-20\Software\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Settings' # The network service account using wmiprvse.exe sets values in the user hive #region Functions function DoesRegistryKeyPropertyExist @@ -65,13 +66,14 @@ function Set-WindowsUpdateRegistryKey $value = [int]$value } - if ($typeInfo -eq 'Int32' -and $key -in @('UserChoiceActiveHoursEnd', 'UserChoiceActiveHoursStart')) - { - if ($value -notin (0..24)) - { - Throw "Value for $key must be between 0 and 24" - } - } + # validate the value of UserChoiceActiveHoursEnd and UserChoiceActiveHoursStart to be between 0 and 24 + Assert-UserChoiceValue -KeyName $key -Value $value + + # validate the value of DownloadRateBackgroundPct, DownloadRateForegroundPct and UpRatePctBandwith to be between 0 and 100 + Assert-RatePercentageValue -KeyName $key -Value $value + + # validate the value of UpRatePctBandwith to be between 5 and 500 + Assert-UpRateValue -KeyName $key -Value $value if (-not (DoesRegistryKeyPropertyExist -Path $Path -Name $key)) { @@ -81,6 +83,71 @@ function Set-WindowsUpdateRegistryKey Write-Verbose -Message "Setting $key to $($RegistryKeyProperty[$key])" Set-ItemProperty -Path $Path -Name $key -Value $value } +} + +function Assert-UpRateValue +{ + param ( + [Parameter(Mandatory)] + [string] $KeyName, + + [Parameter(Mandatory)] + [int] $Value + ) + + if ($KeyName -eq 'UpRatePctBandwidth' -and $Value -notin (5..500)) + { + Throw "You are specifying a percentage value, which must be between 5 and 500. The value you provided is $Value. Please provide a value between 5 and 500." + } +} + +function Assert-RatePercentageValue +{ + param ( + [Parameter(Mandatory)] + [string] $KeyName, + + [Parameter(Mandatory)] + [int] $Value + ) + + if ($KeyName -in ('DownloadRateBackgroundPct', 'DownloadRateForegroundPct', 'UpRatePctBandwidth') -and $Value -notin (0..100)) + { + # TODO: It might be beneficial to add `Reasons` and not throw, only return statement + Throw "You are specifying a percentage value, which must be between 0 and 100. The value you provided is $Value. Please provide a value between 0 and 100." + } +} + +function Assert-UserChoiceValue +{ + param ( + [Parameter(Mandatory)] + [string] $KeyName, + + [Parameter(Mandatory)] + [int] $Value + ) + + if ($KeyName -in ('UserChoiceActiveHoursEnd', 'UserChoiceActiveHoursStart') -and $Value -notin (0..24)) + { + Throw "Value must be between 0 and 24" + } +} + +function Assert-DownloadRate +{ + param ( + [Parameter(Mandatory)] + [hashtable] $Parameters + ) + + if ($Parameters.ContainsKey('DownloadRateBackgroundPct') -or $Parameters.ContainsKey('DownloadRateForegroundPct')) + { + if ($Parameters.ContainsKey('DownloadRateBackgroundBps') -or $Parameters.ContainsKey('DownloadRateForegroundBps')) + { + Throw "Cannot set both DownloadRateBackgroundPct/DownloadRateForegroundPct and DownloadRateBackgroundBps/DownloadRateForegroundBps" + } + } } #endregion Functions @@ -116,7 +183,27 @@ class WindowsUpdate [DscProperty()] [nullable[int]] $UserChoiceActiveHoursStart - # TODO: Add delivery options + [DscProperty()] + [ValidateSet(0, 1, 3)] + [nullable[int]] $DownloadMode + + [DscProperty()] + [nullable[int]] $DownloadRateBackgroundBps + + [DscProperty()] + [nullable[int]] $DownloadRateForegroundBps + + [DscProperty()] + [nullable[int]] $DownloadRateBackgroundPct + + [DscProperty()] + [nullable[int]] $DownloadRateForegroundPct + + [DscProperty()] + [nullable[int]] $UploadLimitGBMonth + + [DscProperty()] + [nullable[int]] $UpRatePctBandwidth static hidden [string] $IsContinuousInnovationOptedInProperty = 'IsContinuousInnovationOptedIn' static hidden [string] $AllowMUUpdateServiceProperty = 'AllowMUUpdateService' @@ -126,6 +213,13 @@ class WindowsUpdate static hidden [string] $SmartActiveHoursStateProperty = 'SmartActiveHoursState' static hidden [string] $UserChoiceActiveHoursEndProperty = 'UserChoiceActiveHoursEnd' static hidden [string] $UserChoiceActiveHoursStartProperty = 'UserChoiceActiveHoursStart' + static hidden [string] $DownloadModeProperty = 'DownloadMode' + static hidden [string] $DownloadRateBackgroundBpsProperty = 'DownloadRateBackgroundBps' + static hidden [string] $DownloadRateForegroundBpsProperty = 'DownloadRateForegroundBps' + static hidden [string] $DownloadRateBackgroundPctProperty = 'DownloadRateBackgroundPct' + static hidden [string] $DownloadRateForegroundPctProperty = 'DownloadRateForegroundPct' + static hidden [string] $UploadLimitGBMonthProperty = 'UploadLimitGBMonth' + static hidden [string] $UpRatePctBandwidthProperty = 'UpRatePctBandwidth' [WindowsUpdate] Get() { @@ -138,6 +232,13 @@ class WindowsUpdate $currentState.SmartActiveHoursState = [WindowsUpdate]::SmartActiveHoursStateStatus() $currentState.UserChoiceActiveHoursEnd = [WindowsUpdate]::UserChoiceActiveHoursEndStatus() $currentState.UserChoiceActiveHoursStart = [WindowsUpdate]::UserChoiceActiveHoursStartStatus() + $currentState.DownloadMode = [WindowsUpdate]::DownloadModeStatus() + $currentState.DownloadRateBackgroundBps = [WindowsUpdate]::DownloadRateBackGroundBps() + $currentState.DownloadRateForegroundBps = [WindowsUpdate]::DownloadRateForegroundBps() + $currentState.DownloadRateBackgroundPct = [WindowsUpdate]::DownloadRateBackgroundPctStatus() + $currentState.DownloadRateForegroundPct = [WindowsUpdate]::DownloadRateForegroundPctStatus() + $currentState.UploadLimitGBMonth = [WindowsUpdate]::UploadLimitGBMonthStatus() + $currentState.UpRatePctBandwidth = [WindowsUpdate]::UpRatePctBandwidthStatus() return $currentState } @@ -158,6 +259,8 @@ class WindowsUpdate $parameters = $this.GetParameters() + Assert-DownloadRate -Parameters $parameters + Set-WindowsUpdateRegistryKey -Path $global:WindowsUpdateSettingPath -RegistryKeyProperty $parameters } @@ -268,6 +371,97 @@ class WindowsUpdate } } + static [int] DownloadModeStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadModeProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadModeProperty) | Select-Object -ExpandProperty DownloadMode + return $value + } + } + + static [int] DownloadRateBackGroundBps() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateBackGroundBpsProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateBackGroundBpsProperty) | Select-Object -ExpandProperty DownloadRateBackGroundBps + return $value + } + } + + static [int] DownloadRateForegroundBps() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateForegroundBpsProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateForegroundBpsProperty) | Select-Object -ExpandProperty DownloadRateForegroundBps + return $value + } + } + + static [int] DownloadRateBackgroundPctStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateBackgroundPctProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateBackgroundPctProperty) | Select-Object -ExpandProperty DownloadRateBackgroundPct + return $value + } + } + + static [int] DownloadRateForegroundPctStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateForegroundPctProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateForegroundPctProperty) | Select-Object -ExpandProperty DownloadRateForegroundPct + return $value + } + } + + static [int] UploadLimitGBMonthStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::UploadLimitGBMonthProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::UploadLimitGBMonthProperty) | Select-Object -ExpandProperty UploadLimitGBMonth + return $value + } + } + + static [int] UpRatePctBandwidthStatus() + { + if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::UpRatePctBandwidthProperty))) + { + return $false + } + else + { + $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::UpRatePctBandwidthProperty) | Select-Object -ExpandProperty UpRatePctBandwidth + return $value + } + } + [hashtable] GetParameters() { $parameters = @{} From 81b64c375857e4a87febb8dae8e2620920e4e5cf Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 13:10:04 +0100 Subject: [PATCH 13/33] Update the docs --- .../WindowsUpdate.md | 0 ...crosoft.Windows.Setting.WindowsUpdate.psm1 | 57 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 resources/Help/Microsoft.Windows.Setting.WindowsUpdate/WindowsUpdate.md diff --git a/resources/Help/Microsoft.Windows.Setting.WindowsUpdate/WindowsUpdate.md b/resources/Help/Microsoft.Windows.Setting.WindowsUpdate/WindowsUpdate.md new file mode 100644 index 00000000..e69de29b diff --git a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 index bd60c883..a20b9e4f 100644 --- a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 +++ b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 @@ -152,6 +152,63 @@ function Assert-DownloadRate #endregion Functions #region Classes +<# +.SYNOPSIS + The `WindowsUpdate` DSC resource allows you to configure various Windows Update settings, including enabling or disabling specific update services, setting download and upload rates, and configuring active hours for updates. + +.PARAMETER SID + The security identifier. This is a key property and should not be set manually. + +.PARAMETER IsContinuousInnovationOptedIn + Indicates whether the device is opted in for continuous innovation updates. This is the setting in Windows Update settings -> Get the latest updates as soon as they're available. + +.PARAMETER AllowMUUpdateService + Indicates whether the Microsoft Update service is allowed. This is the setting in Windows Update settings -> Advanced options -> Receive updates for other Microsoft products. + +.PARAMETER IsExpedited + Indicates whether the updates are expedited. This is the setting in Windows Update settings -> Advanced options -> Get me up to date. + +.PARAMETER AllowAutoWindowsUpdateDownloadOverMeteredNetwork + Indicates whether automatic Windows Update downloads are allowed over metered networks. This is the setting in Windows Update settings -> Advanced options -> Download updates over metered connections. + +.PARAMETER RestartNotificationsAllowed + Indicates whether restart notifications are allowed. This is the setting in Windows Update settings -> Advanced options -> Notify me when a restart is required to finish updating. + +.PARAMETER SmartActiveHoursState + Indicates whether smart active hours are enabled. + +.PARAMETER UserChoiceActiveHoursEnd + The end time for user-chosen active hours. + +.PARAMETER UserChoiceActiveHoursStart + The start time for user-chosen active hours. + +.PARAMETER DownloadMode + The download mode for updates. Valid values are 0, 1, and 3. This is the setting in Windows Update settings -> Advanced options -> Delivery Optimization -> Allow downloads from other PCs. + +.PARAMETER DownloadRateBackgroundBps + The background download rate in bits per second. + +.PARAMETER DownloadRateForegroundBps + The foreground download rate in bits per second. + +.PARAMETER DownloadRateBackgroundPct + The background download rate as a percentage. + +.PARAMETER DownloadRateForegroundPct + The foreground download rate as a percentage. + +.PARAMETER UploadLimitGBMonth + The upload limit in gigabytes per month. + +.PARAMETER UpRatePctBandwidth + The upload rate as a percentage of bandwidth. + +.EXAMPLE + PS C:\> Invoke-DscResource -Name WindowsUpdate -Method Get -ModuleName Microsoft.Windows.Setting.WindowsUpdate -Property @{} + + This command gets the current Windows Update settings. +#> [DSCResource()] class WindowsUpdate { From 059b59bd630a6f5baefb4c2922cf73f0f2135427 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 13:22:45 +0100 Subject: [PATCH 14/33] Update table values --- .../WindowsUpdate.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/resources/Help/Microsoft.Windows.Setting.WindowsUpdate/WindowsUpdate.md b/resources/Help/Microsoft.Windows.Setting.WindowsUpdate/WindowsUpdate.md index e69de29b..9fa02660 100644 --- a/resources/Help/Microsoft.Windows.Setting.WindowsUpdate/WindowsUpdate.md +++ b/resources/Help/Microsoft.Windows.Setting.WindowsUpdate/WindowsUpdate.md @@ -0,0 +1,50 @@ +--- +external help file: Microsoft.Windows.Setting.Update.psm1-Help.xml +Module Name: Microsoft.Windows.Setting.Update +ms.date: 11/04/2024 +online version: +schema: 2.0.0 +title: WindowsUpdate +--- + +# WindowsUpdate + +## SYNOPSIS + +The `WindowsUpdate` DSC resource allows you to configure various Windows Update settings, including enabling or disabling specific update services, setting download and upload rates, and configuring active hours for updates. + +## DESCRIPTION + +The `WindowsUpdate` DSC resource allows you to configure various Windows Update settings, including enabling or disabling specific update services, setting download and upload rates, and configuring active hours for updates. + +## PARAMETERS + +| **Parameter** | **Attribute** | **DataType** | **Description** | **Allowed Values** | +| -------------------------------------------------- | ------------- | ------------ | -------------------------------------------------------------------------------- | ----------------------------------------------- | +| `SID` | Key | String | The security identifier. This is a key property and should not be set manually. | N/A | +| `IsContinuousInnovationOptedIn` | Optional | Boolean | Indicates whether the device is opted in to continuous innovation updates. | `$true`, `$false` | +| `AllowMUUpdateService` | Optional | Boolean | Allows updates from Microsoft Update service. | `$true`, `$false` | +| `IsExpedited` | Optional | Boolean | Indicates whether updates should be expedited. | `$true`, `$false` | +| `AllowAutoWindowsUpdateDownloadOverMeteredNetwork` | Optional | Boolean | Allows automatic Windows Update downloads over metered networks. | `$true`, `$false` | +| `RestartNotificationsAllowed` | Optional | Boolean | Allows restart notifications for updates. | `$true`, `$false` | +| `SmartActiveHoursState` | Optional | String | Configures smart active hours state for updates. | `Enabled`, `Disabled` | +| `UserChoiceActiveHoursEnd` | Optional | Integer | Specifies the end time for user-chosen active hours in `HH:MM` format. | Any valid time in `HH:MM` format | +| `UserChoiceActiveHoursStart` | Optional | Integer | Specifies the start time for user-chosen active hours in `HH:MM` format. | Any valid time in `HH:MM` format | +| `DownloadMode` | Optional | Integer | Specifies the download mode for updates. | `Foreground`, `Background`, `Bypass`, `None` | +| `DownloadRateBackgroundBps` | Optional | Integer | Specifies the background download rate for updates in Bps. | Any positive integer value. E.g. 20000 is 2MBPs | +| `DownloadRateForegroundBps` | Optional | Integer | Specifies the foreground download rate for updates in Bps. | Any positive integer value | +| `DownloadRateBackgroundPct` | Optional | Integer | Specifies the background download rate for updates as a percentage of bandwidth. | 0-100 | +| `DownloadRateForegroundPct` | Optional | Integer | Specifies the foreground download rate for updates as a percentage of bandwidth. | 0-100 | +| `UploadLimitGBMonth` | Optional | Integer | Specifies the upload limit for updates in GB per month. | 5-500 | +| `UpRatePctBandwidth` | Optional | Integer | Specifies the upload rate as a percentage of bandwidth. | 0-100 | + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +$params = @{} +Invoke-DscResource -Name WindowsUpdate -Method Set -Property $params -ModuleName Microsoft.Windows.Setting.WindowsUpdate + +# This command gets the current Windows Update settings. +``` From 4541bb4a8011900c08d9072aa8f2e44ffc5b869d Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Mon, 4 Nov 2024 14:12:57 +0100 Subject: [PATCH 15/33] Remove duplicate code --- ...crosoft.Windows.Setting.WindowsUpdate.psm1 | 256 ++++-------------- 1 file changed, 48 insertions(+), 208 deletions(-) diff --git a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 index a20b9e4f..f26d1e87 100644 --- a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 +++ b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 @@ -1,5 +1,8 @@ $global:WindowsUpdateSettingPath = 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings' -$global:DeliveryOptimizationSettingPath = 'Registry::HKEY_USERS\S-1-5-20\Software\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Settings' # The network service account using wmiprvse.exe sets values in the user hive +# The network service account using wmiprvse.exe sets values in the user hive. This is the path to the Delivery Optimization settings in the user hive. +# It requires elevation to read the values +# Other settings might be needed e.g. DownloadRateForegroundProvider, DownloadRateBackgroundProvider +$global:DeliveryOptimizationSettingPath = 'Registry::HKEY_USERS\S-1-5-20\Software\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Settings' #region Functions function DoesRegistryKeyPropertyExist @@ -149,6 +152,37 @@ function Assert-DownloadRate } } } + +function Initialize-WindowsUpdate +{ + $class = [WindowsUpdate]::new() + + $hiddenProperties = $class | Get-Member -Static -Force | Where-Object { $_.MemberType -eq 'Property' } | Select-Object -ExpandProperty Name + + foreach ($p in $hiddenProperties) + { + $classPropertyName = $p.Replace("Property", "") + $dataType = $class | Get-Member | Where-Object { $_.Name -eq $classPropertyName } | Select-Object -ExpandProperty Definition | Select-String -Pattern '\[.*\]' | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty Value + + $currentValue = [WindowsUpdate]::GetRegistryValue($class::$p) + if ($null -eq $currentValue) + { + if ($dataType -eq '[bool]') + { + $currentValue = $false + } + + if ($dataType -eq '[int]') + { + $currentValue = 0 + } + } + + $class.$classPropertyName = $currentValue + } + + return $class +} #endregion Functions #region Classes @@ -280,22 +314,7 @@ class WindowsUpdate [WindowsUpdate] Get() { - $currentState = [WindowsUpdate]::new() - $currentState.IsContinuousInnovationOptedIn = [WindowsUpdate]::GetIsContinuousInnovationOptedInStatus() - $currentState.AllowMUUpdateService = [WindowsUpdate]::AllowMUUpdateServiceStatus() - $currentState.IsExpedited = [WindowsUpdate]::IsExpeditedStatus() - $currentState.AllowAutoWindowsUpdateDownloadOverMeteredNetwork = [WindowsUpdate]::AllowAutoWindowsUpdateDownloadOverMeteredNetworkStatus() - $currentState.RestartNotificationsAllowed = [WindowsUpdate]::RestartNotificationsAllowedStatus() - $currentState.SmartActiveHoursState = [WindowsUpdate]::SmartActiveHoursStateStatus() - $currentState.UserChoiceActiveHoursEnd = [WindowsUpdate]::UserChoiceActiveHoursEndStatus() - $currentState.UserChoiceActiveHoursStart = [WindowsUpdate]::UserChoiceActiveHoursStartStatus() - $currentState.DownloadMode = [WindowsUpdate]::DownloadModeStatus() - $currentState.DownloadRateBackgroundBps = [WindowsUpdate]::DownloadRateBackGroundBps() - $currentState.DownloadRateForegroundBps = [WindowsUpdate]::DownloadRateForegroundBps() - $currentState.DownloadRateBackgroundPct = [WindowsUpdate]::DownloadRateBackgroundPctStatus() - $currentState.DownloadRateForegroundPct = [WindowsUpdate]::DownloadRateForegroundPctStatus() - $currentState.UploadLimitGBMonth = [WindowsUpdate]::UploadLimitGBMonthStatus() - $currentState.UpRatePctBandwidth = [WindowsUpdate]::UpRatePctBandwidthStatus() + $currentState = Initialize-WindowsUpdate return $currentState } @@ -322,201 +341,22 @@ class WindowsUpdate } #region WindowsUpdate helper functions - static [bool] GetIsContinuousInnovationOptedInStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::IsContinuousInnovationOptedInProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::IsContinuousInnovationOptedInProperty).IsContinuousInnovationOptedInProperty - return $value - } - } - - static [bool] AllowMUUpdateServiceStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::AllowMUUpdateServiceProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::AllowMUUpdateServiceProperty).AllowMUUpdateServiceProperty - return $value - } - } - - static [bool] IsExpeditedStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::IsExpeditedProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::IsExpeditedProperty).IsExpeditedProperty - return $value - } - } - - static [bool] AllowAutoWindowsUpdateDownloadOverMeteredNetworkStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::AllowAutoWindowsUpdateDownloadOverMeteredNetworkProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::AllowAutoWindowsUpdateDownloadOverMeteredNetworkProperty).AllowAutoWindowsUpdateDownloadOverMeteredNetworkProperty - return $value - } - } - - static [bool] RestartNotificationsAllowedStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::RestartNotificationsAllowedProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::RestartNotificationsAllowedProperty).RestartNotificationsAllowed - return $value - } - } - - static [bool] SmartActiveHoursStateStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::SmartActiveHoursStateProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::SmartActiveHoursStateProperty).SmartActiveHoursState - return $value - } - } - - static [int] UserChoiceActiveHoursEndStatus() + static [object] GetRegistryValue($PropertyName) { - if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::UserChoiceActiveHoursEndProperty))) + $value = $null + if ($null -ne $PropertyName) { - return $false - } - else - { - # there is some weird behaviour with integers in the registry, so we need to get the value from the property - $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::UserChoiceActiveHoursEndProperty) | Select-Object -ExpandProperty UserChoiceActiveHoursEnd - - return $value - } - } - - static [int] UserChoiceActiveHoursStartStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::UserChoiceActiveHoursStartProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name ([WindowsUpdate]::UserChoiceActiveHoursStartProperty) | Select-Object -ExpandProperty UserChoiceActiveHoursStart - return $value - } - } - - static [int] DownloadModeStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadModeProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadModeProperty) | Select-Object -ExpandProperty DownloadMode - return $value - } - } - - static [int] DownloadRateBackGroundBps() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateBackGroundBpsProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateBackGroundBpsProperty) | Select-Object -ExpandProperty DownloadRateBackGroundBps - return $value - } - } - - static [int] DownloadRateForegroundBps() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateForegroundBpsProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateForegroundBpsProperty) | Select-Object -ExpandProperty DownloadRateForegroundBps - return $value - } - } - - static [int] DownloadRateBackgroundPctStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateBackgroundPctProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateBackgroundPctProperty) | Select-Object -ExpandProperty DownloadRateBackgroundPct - return $value - } - } - - static [int] DownloadRateForegroundPctStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateForegroundPctProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::DownloadRateForegroundPctProperty) | Select-Object -ExpandProperty DownloadRateForegroundPct - return $value - } - } - - static [int] UploadLimitGBMonthStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::UploadLimitGBMonthProperty))) - { - return $false + if ((DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name $PropertyName)) + { + $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name $PropertyName | Select-Object -ExpandProperty $PropertyName + } + elseif ((DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name $PropertyName)) + { + $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name $PropertyName | Select-Object -ExpandProperty $PropertyName + } } - else - { - $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::UploadLimitGBMonthProperty) | Select-Object -ExpandProperty UploadLimitGBMonth - return $value - } - } - static [int] UpRatePctBandwidthStatus() - { - if (-not(DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::UpRatePctBandwidthProperty))) - { - return $false - } - else - { - $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name ([WindowsUpdate]::UpRatePctBandwidthProperty) | Select-Object -ExpandProperty UpRatePctBandwidth - return $value - } + return $value } [hashtable] GetParameters() From 102c763c177fd55e720d71c0daf4a4ed2d179422 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 5 Nov 2024 02:35:45 +0100 Subject: [PATCH 16/33] Add whatif tests --- resources/PythonPip3Dsc/PythonPip3Dsc.psm1 | 34 ++++++++++----------- tests/PythonPip3Dsc/PythonPip3Dsc.Tests.ps1 | 27 ++++++++++++++++ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 b/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 index 4e884164..cb15fc6c 100644 --- a/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 +++ b/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 @@ -15,10 +15,7 @@ function Invoke-Process [Parameter()] [ValidateNotNullOrEmpty()] - [string]$ArgumentList, - - [ValidateSet("Full", "StdOut", "StdErr", "ExitCode", "None")] - [string]$DisplayLevel + [string]$ArgumentList ) try @@ -36,6 +33,7 @@ function Invoke-Process $p.Start() | Out-Null $stOut = @() + # using ReadLine() instead of ReadToEnd() for building array object. ReadToEnd() gave different output than ReadLine() in some cases. while (-not $p.StandardOutput.EndOfStream) { $stOut += $p.StandardOutput.ReadLine() @@ -58,20 +56,12 @@ function Invoke-Process $p.WaitForExit() - if (-not([string]::IsNullOrEmpty($DisplayLevel))) - { - switch ($DisplayLevel) - { - "Full" { return $result; break } - "StdOut" { return $result.StdOut; break } - "StdErr" { return $result.StdErr; break } - "ExitCode" { return $result.ExitCode; break } - } - } + return $result } catch { - exit 1 + Write-Verbose -Message "Error occurred while executing the command: $FilePath $ArgumentList. Error:" + Write-Verbose -Message $stErr } } @@ -203,9 +193,11 @@ function Invoke-Pip3Install [Parameter()] [string]$Version, + # not explicitly used, only to call from lower functions if parameters are passed [Parameter()] [switch]$IsUpdate, + # not explicitly used, only to call from lower functions if parameters are passed [Parameter()] [switch]$DryRun ) @@ -335,11 +327,11 @@ function Invoke-Pip3 if ($global:usePip3Exe) { - return Invoke-Process -FilePath $global:pip3ExePath -ArgumentList $command -DisplayLevel Full + return Invoke-Process -FilePath $global:pip3ExePath -ArgumentList $command } else { - return Invoke-Process -FilePath pip3 -ArgumentList $command -DisplayLevel Full + return Invoke-Process -FilePath pip3 -ArgumentList $command } } @@ -491,10 +483,16 @@ class Pip3Package { $whatIfState = Invoke-Pip3Install -PackageName $this.PackageName -Version $this.Version -Arguments $this.Arguments -DryRun + $whatIfResult = $whatIfState.StdOut + if ($whatIfState.ExitCode -ne 0) + { + $whatIfResult = $whatIfState.StdErr + } + $out = @{ PackageName = $this.PackageName _metaData = @{ - whatIf = $whatIfState.StdOut + whatIf = $whatIfResult } } } diff --git a/tests/PythonPip3Dsc/PythonPip3Dsc.Tests.ps1 b/tests/PythonPip3Dsc/PythonPip3Dsc.Tests.ps1 index 339e6221..121400b9 100644 --- a/tests/PythonPip3Dsc/PythonPip3Dsc.Tests.ps1 +++ b/tests/PythonPip3Dsc/PythonPip3Dsc.Tests.ps1 @@ -104,4 +104,31 @@ Describe 'Pip3Package' { $finalState = Invoke-DscResource -Name Pip3Package -ModuleName PythonPip3Dsc -Method Get -Property $desiredState $finalState.Exist | Should -BeFalse } + + It 'Performs whatif operation successfully' -Skip:(!$IsWindows) { + $whatIfState = @{ + PackageName = 'itsdangerous' + Version = '2.2.0' + } + + $pipPackage = [Pip3Package]$whatIfState + $whatIf = $pipPackage.WhatIf() | ConvertFrom-Json + + + $whatIf.PackageName | Should -Be 'itsdangerous' + $whatIf._metaData.whatIf | Should -Contain "Would install itsdangerous-$($whatIfState.Version)" + } + + It 'Does not return whatif result if package is invalid' -Skip:(!$IsWindows) { + $whatIfState = @{ + PackageName = 'itsdangerouss' + } + + $pipPackage = [Pip3Package]$whatIfState + $whatIf = $pipPackage.WhatIf() | ConvertFrom-Json + + + $whatIf.PackageName | Should -Be 'itsdangerouss' + $whatIf._metaData.whatIf | Should -Contain "ERROR: No matching distribution found for $($whatIfState.PackageName)" + } } From f8270f78f6ab2c43541ab151deaf4fa7223dd624 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 5 Nov 2024 02:57:53 +0100 Subject: [PATCH 17/33] Small typo in docs --- .../Help/Microsoft.Windows.Setting.Language/DisplayLanguage.md | 2 +- resources/Help/Microsoft.Windows.Setting.Language/Language.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/Help/Microsoft.Windows.Setting.Language/DisplayLanguage.md b/resources/Help/Microsoft.Windows.Setting.Language/DisplayLanguage.md index 100d8cd9..6c42959b 100644 --- a/resources/Help/Microsoft.Windows.Setting.Language/DisplayLanguage.md +++ b/resources/Help/Microsoft.Windows.Setting.Language/DisplayLanguage.md @@ -22,7 +22,7 @@ The `DisplayLanguage` DSC Resource allows you to set the display language on you | **Parameter** | **Attribute** | **DataType** | **Description** | **Allowed Values** | | ------------- | ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `LocaleName` | Mandatory | String | The name of the language. This is the language tag that represents the language. For example, `en-US` represents English (United States). | Use the `Get-WinUserLanguageList` to see what language pack have been installed. | -| `Exist` | Optional | Boolean | Indicates whether the extension should exist. The default value is `$true`. | `$true`, `$false` | +| `Exist` | Optional | Boolean | Indicates whether the language should exist. The default value is `$true`. | `$true`, `$false` | ## EXAMPLES diff --git a/resources/Help/Microsoft.Windows.Setting.Language/Language.md b/resources/Help/Microsoft.Windows.Setting.Language/Language.md index ccea48a1..79439791 100644 --- a/resources/Help/Microsoft.Windows.Setting.Language/Language.md +++ b/resources/Help/Microsoft.Windows.Setting.Language/Language.md @@ -22,7 +22,7 @@ The `Language` DSC Resource allows you to install, update, and uninstall languag | **Parameter** | **Attribute** | **DataType** | **Description** | **Allowed Values** | | ------------- | ------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `LocaleName` | Mandatory | String | The name of the language. This is the language tag that represents the language. For example, `en-US` represents English (United States). | Use the `Get-LocaleList` function or Export() method to get a list of allowed values. | -| `Exist` | Optional | Boolean | Indicates whether the extension should exist. The default value is `$true`. | `$true`, `$false` | +| `Exist` | Optional | Boolean | Indicates whether the language should exist. The default value is `$true`. | `$true`, `$false` | ## EXAMPLES From 5f01ee25cf6e74deaabe13934a6a7eca0473c3e7 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 5 Nov 2024 06:37:45 +0100 Subject: [PATCH 18/33] Initial setup for Microsoft.Windows.Setting.Time --- .../Microsoft.Windows.Setting.Time.psd1 | 132 +++++++++++++++ .../Microsoft.Windows.Setting.Time.psm1 | 150 ++++++++++++++++++ .../Microsoft.Windows.Setting.Time.Tests.ps1 | 0 3 files changed, 282 insertions(+) create mode 100644 resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 create mode 100644 resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 create mode 100644 tests/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.Tests.ps1 diff --git a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 new file mode 100644 index 00000000..9da2bbe4 --- /dev/null +++ b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 @@ -0,0 +1,132 @@ +# +# Module manifest for module 'Microsoft.Windows.Setting.Time' +# +# Generated by: Microsoft Corporation +# +# Generated on: 05/11/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'Microsoft.Windows.Setting.Time.psm1' + +# Version number of this module. +ModuleVersion = '0.1.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '6a947f86-eb17-46a4-9b0d-9f757b19c29a' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'DSC Resource for Windows Time Settings' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.2' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = '*' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-dsc' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + Prerelease = 'alpha' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 new file mode 100644 index 00000000..a54bcc5b --- /dev/null +++ b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 @@ -0,0 +1,150 @@ +$global:tzAutoUpdatePath = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters" +$global:SysTrayPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" + +#region Functions +function TryGetRegistryValue +{ + param ( + [Parameter(Mandatory = $true)] + [string]$Key, + + [Parameter(Mandatory = $true)] + [string]$Property + ) + + if (Test-Path -Path $Key) + { + try + { + return (Get-ItemProperty -Path $Key -Name $Property -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $Property) + } + catch + { + Write-Verbose "Property `"$($Property)`" could not be found." + } + } + else + { + Write-Verbose "Registry key does not exist." + } +} +#endRegion Functions + +#region Enum +enum TimeZoneAutomatically +{ + NTP + NoSync +} + +#region Classes +[DscResource()] +class Time +{ + [DscProperty(Key)] + [string] $Sid + + [DscProperty()] + [TimeZoneAutomatically] $SetTimeZoneAutomatically = [TimeZoneAutomatically]::NTP + + [DscProperty()] + [string] $TimeZone + + [DscProperty()] + [nullable[bool]] $ShowSystemTrayDateTime + + static hidden [string] $SetTimeZoneAutomaticallyProperty = 'Type' + static hidden [string] $ShowSystemTrayDateTimeProperty = 'ShowSystrayDateTimeValueName' + + [Time] Get() + { + $currentState = [Time]::New() + $currentState.SetTimeZoneAutomatically = [Time]::GetTimeZoneAutoUpdateStatus() + $currentState.TimeZone = (Get-TimeZone).Id + $currentState.ShowSystemTrayDateTime = [Time]::GetShowSystemTrayDateTimeStatus() + + return $currentState + } + + [void] Set() + { + if ($this.Test()) + { + return + } + + if ($null -ne $this.SetTimeZoneAutomatically) + { + Set-ItemProperty -Path $global:tzAutoUpdatePath -Name ([Time]::SetTimeZoneAutomaticallyProperty) -Value $this.SetTimeZoneAutomatically + } + + if ($null -ne $this.TimeZone) + { + Set-TimeZone -Id $this.TimeZone + } + + if ($null -ne $this.ShowSystemTrayDateTime) + { + if ($this.ShowSystemTrayDateTime) + { + Set-ItemProperty -Path $global:SysTrayPath -Name ([Time]::ShowSystemTrayDateTimeProperty) -Value 1 # 1 = Show + } + else + { + Set-ItemProperty -Path $global:SysTrayPath -Name ([Time]::ShowSystemTrayDateTimeProperty) -Value 0 # 0 = Hide + } + } + } + + [bool] Test() + { + $currentState = $this.Get() + + if (($null -ne $this.ShowSystemTrayDateTime) -and ($this.ShowSystemTrayDateTime -ne $currentState.ShowSystemTrayDateTime)) + { + return $false + } + + if (($null -ne $this.TimeZone) -and ($this.TimeZone -ne $currentState.TimeZone)) + { + return $false + } + + if (($null -ne $this.SetTimeZoneAutomatically) -and ($this.SetTimeZoneAutomatically -ne $currentState.SetTimeZoneAutomatically)) + { + return $false + } + + return $true + } + + #region Time helper functions + static [TimeZoneAutomatically] GetTimeZoneAutoUpdateStatus() + { + # key should actually always be present, but we'll check anyway + $keyValue = TryGetRegistryValue -Key $global:tzAutoUpdatePath -Property ([Time]::SetTimeZoneAutomaticallyProperty) + if ($null -eq $keyValue) + { + return $false + } + else + { + return ($keyValue -as [TimeZoneAutomatically]) + } + } + + static [bool] GetShowSystemTrayDateTimeStatus() + { + $value = TryGetRegistryValue -Key $global:SysTrayPath -Property ([Time]::ShowSystemTrayDateTimeProperty) + if ($null -ne $value) + { + return $false + } + else + { + return ($value -eq 1) + } + } + #endRegion Time helper functions +} +#endRegion Classes \ No newline at end of file diff --git a/tests/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.Tests.ps1 b/tests/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.Tests.ps1 new file mode 100644 index 00000000..e69de29b From 514868e6d5db74615ebbf07098bd5fe02ff41b0c Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 5 Nov 2024 07:13:08 +0100 Subject: [PATCH 19/33] Fix up on validateset --- .../Microsoft.Windows.Setting.Time.psd1 | 159 +++++++++--------- .../Microsoft.Windows.Setting.Time.psm1 | 70 ++++++-- 2 files changed, 133 insertions(+), 96 deletions(-) diff --git a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 index 9da2bbe4..6d1223a2 100644 --- a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 +++ b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 @@ -8,125 +8,124 @@ @{ -# Script module or binary module file associated with this manifest. -RootModule = 'Microsoft.Windows.Setting.Time.psm1' + # Script module or binary module file associated with this manifest. + RootModule = 'Microsoft.Windows.Setting.Time.psm1' -# Version number of this module. -ModuleVersion = '0.1.0' + # Version number of this module. + ModuleVersion = '0.1.0' -# Supported PSEditions -# CompatiblePSEditions = @() + # Supported PSEditions + # CompatiblePSEditions = @() -# ID used to uniquely identify this module -GUID = '6a947f86-eb17-46a4-9b0d-9f757b19c29a' + # ID used to uniquely identify this module + GUID = '6a947f86-eb17-46a4-9b0d-9f757b19c29a' -# Author of this module -Author = 'Microsoft Corporation' + # Author of this module + Author = 'Microsoft Corporation' -# Company or vendor of this module -CompanyName = 'Microsoft Corporation' + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' -# Copyright statement for this module -Copyright = '(c) Microsoft Corporation. All rights reserved.' + # Copyright statement for this module + Copyright = '(c) Microsoft Corporation. All rights reserved.' -# Description of the functionality provided by this module -Description = 'DSC Resource for Windows Time Settings' + # Description of the functionality provided by this module + Description = 'DSC Resource for Windows Time Settings' -# Minimum version of the PowerShell engine required by this module -PowerShellVersion = '7.2' + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '7.2' -# Name of the PowerShell host required by this module -# PowerShellHostName = '' + # Name of the PowerShell host required by this module + # PowerShellHostName = '' -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# ClrVersion = '' + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = '*' + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = '*' -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = '*' + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = '*' -# Variables to export from this module -VariablesToExport = '*' + # Variables to export from this module + VariablesToExport = '*' -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '*' + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = '*' -# DSC resources to export from this module -# DscResourcesToExport = @() + # DSC resources to export from this module + DscResourcesToExport = @('Time') -# List of all modules packaged with this module -# ModuleList = @() + # List of all modules packaged with this module + # ModuleList = @() -# List of all files packaged with this module -# FileList = @() + # List of all files packaged with this module + # FileList = @() -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ - PSData = @{ + PSData = @{ - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('PSDscResource_Time') - # A URL to the license for this module. - LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' - # A URL to the main website for this project. - ProjectUri = 'https://github.com/microsoft/winget-dsc' + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-dsc' - # A URL to an icon representing this module. - # IconUri = '' + # A URL to an icon representing this module. + # IconUri = '' - # ReleaseNotes of this module - # ReleaseNotes = '' + # ReleaseNotes of this module + # ReleaseNotes = '' - # Prerelease string of this module - Prerelease = 'alpha' + # Prerelease string of this module + Prerelease = 'alpha' - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false - # External dependent modules of this module - # ExternalModuleDependencies = @() + # External dependent modules of this module + # ExternalModuleDependencies = @() - } # End of PSData hashtable + } # End of PSData hashtable -} # End of PrivateData hashtable + } # End of PrivateData hashtable -# HelpInfo URI of this module -# HelpInfoURI = '' - -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' + # HelpInfo URI of this module + # HelpInfoURI = '' + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' } diff --git a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 index a54bcc5b..86a16757 100644 --- a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 +++ b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 @@ -33,7 +33,7 @@ function TryGetRegistryValue #region Enum enum TimeZoneAutomatically { - NTP + NTP NoSync } @@ -41,15 +41,45 @@ enum TimeZoneAutomatically [DscResource()] class Time { + # Timezone values are taken from the list of timezones (Get-TimeZone -ListAvailable).Id [DscProperty(Key)] - [string] $Sid + [ValidateSet( + "Dateline Standard Time", "UTC-11", "Aleutian Standard Time", "Hawaiian Standard Time", "Marquesas Standard Time", + "Alaskan Standard Time", "UTC-09", "Pacific Standard Time (Mexico)", "UTC-08", "Pacific Standard Time", + "US Mountain Standard Time", "Mountain Standard Time (Mexico)", "Mountain Standard Time", "Central America Standard Time", + "Central Standard Time", "Easter Island Standard Time", "Central Standard Time (Mexico)", "Canada Central Standard Time", + "SA Pacific Standard Time", "Eastern Standard Time (Mexico)", "Eastern Standard Time", "Haiti Standard Time", + "Cuba Standard Time", "US Eastern Standard Time", "Turks And Caicos Standard Time", "Paraguay Standard Time", + "Atlantic Standard Time", "Venezuela Standard Time", "Central Brazilian Standard Time", "SA Western Standard Time", + "Pacific SA Standard Time", "Newfoundland Standard Time", "Tocantins Standard Time", "E. South America Standard Time", + "SA Eastern Standard Time", "Argentina Standard Time", "Greenland Standard Time", "Montevideo Standard Time", + "Magallanes Standard Time", "Saint Pierre Standard Time", "Bahia Standard Time", "UTC-02", "Mid-Atlantic Standard Time", + "Azores Standard Time", "Cape Verde Standard Time", "UTC", "Morocco Standard Time", "GMT Standard Time", + "Greenwich Standard Time", "W. Europe Standard Time", "Central Europe Standard Time", "Romance Standard Time", + "Central European Standard Time", "W. Central Africa Standard Time", "Namibia Standard Time", "Jordan Standard Time", + "GTB Standard Time", "Middle East Standard Time", "Egypt Standard Time", "E. Europe Standard Time", "Syria Standard Time", + "West Bank Standard Time", "South Africa Standard Time", "FLE Standard Time", "Israel Standard Time", "Kaliningrad Standard Time", + "Sudan Standard Time", "Libya Standard Time", "Namibia Standard Time", "Arabic Standard Time", "Turkey Standard Time", + "Arab Standard Time", "Belarus Standard Time", "Russian Standard Time", "E. Africa Standard Time", "Iran Standard Time", + "Arabian Standard Time", "Astrakhan Standard Time", "Azerbaijan Standard Time", "Russia Time Zone 3", "Mauritius Standard Time", + "Saratov Standard Time", "Georgian Standard Time", "Caucasus Standard Time", "Afghanistan Standard Time", "West Asia Standard Time", + "Ekaterinburg Standard Time", "Pakistan Standard Time", "India Standard Time", "Sri Lanka Standard Time", "Nepal Standard Time", + "Central Asia Standard Time", "Bangladesh Standard Time", "Omsk Standard Time", "Myanmar Standard Time", "SE Asia Standard Time", + "Altai Standard Time", "W. Mongolia Standard Time", "North Asia Standard Time", "N. Central Asia Standard Time", + "Tomsk Standard Time", "China Standard Time", "North Asia East Standard Time", "Singapore Standard Time", "W. Australia Standard Time", + "Taipei Standard Time", "Ulaanbaatar Standard Time", "North Korea Standard Time", "Aus Central W. Standard Time", + "Transbaikal Standard Time", "Tokyo Standard Time", "Korea Standard Time", "Yakutsk Standard Time", "Cen. Australia Standard Time", + "AUS Central Standard Time", "E. Australia Standard Time", "AUS Eastern Standard Time", "West Pacific Standard Time", + "Tasmania Standard Time", "Vladivostok Standard Time", "Lord Howe Standard Time", "Bougainville Standard Time", + "Russia Time Zone 10", "Magadan Standard Time", "Norfolk Standard Time", "Sakhalin Standard Time", "Central Pacific Standard Time", + "Russia Time Zone 11", "New Zealand Standard Time", "UTC+12", "Fiji Standard Time", "Kamchatka Standard Time", + "Chatham Islands Standard Time", "UTC+13", "Tonga Standard Time", "Samoa Standard Time", "Line Islands Standard Time" + )] + [string] $TimeZone = (Get-TimeZone).Id [DscProperty()] [TimeZoneAutomatically] $SetTimeZoneAutomatically = [TimeZoneAutomatically]::NTP - [DscProperty()] - [string] $TimeZone - [DscProperty()] [nullable[bool]] $ShowSystemTrayDateTime @@ -73,26 +103,33 @@ class Time return } - if ($null -ne $this.SetTimeZoneAutomatically) + $currentState = $this.Get() + + if ($currentState.SetTimeZoneAutomatically -ne $this.SetTimeZoneAutomatically) { Set-ItemProperty -Path $global:tzAutoUpdatePath -Name ([Time]::SetTimeZoneAutomaticallyProperty) -Value $this.SetTimeZoneAutomatically } - if ($null -ne $this.TimeZone) + if ($currentState.TimeZone -ne $this.TimeZone) { Set-TimeZone -Id $this.TimeZone } - if ($null -ne $this.ShowSystemTrayDateTime) + if ($currentState.ShowSystemTrayDateTime -ne $this.ShowSystemTrayDateTime) { - if ($this.ShowSystemTrayDateTime) + $desiredState = switch ($this.ShowSystemTrayDateTime) { - Set-ItemProperty -Path $global:SysTrayPath -Name ([Time]::ShowSystemTrayDateTimeProperty) -Value 1 # 1 = Show + $true { "1" } # 1 = Show + $false { "0" } # 0 = Hide } - else - { - Set-ItemProperty -Path $global:SysTrayPath -Name ([Time]::ShowSystemTrayDateTimeProperty) -Value 0 # 0 = Hide + + if ([string]::IsNullOrEmpty((TryGetRegistryValue -Key $global:SysTrayPath -Property ([Time]::ShowSystemTrayDateTimeProperty)))) + { + New-ItemProperty -Path $global:SysTrayPath -Name ([Time]::ShowSystemTrayDateTimeProperty) -Value $desiredState -PropertyType DWORD + return } + + Set-ItemProperty -Path $global:SysTrayPath -Name ([Time]::ShowSystemTrayDateTimeProperty) -Value $desiredState } } @@ -125,7 +162,7 @@ class Time $keyValue = TryGetRegistryValue -Key $global:tzAutoUpdatePath -Property ([Time]::SetTimeZoneAutomaticallyProperty) if ($null -eq $keyValue) { - return $false + return [TimeZoneAutomatically]::NTP } else { @@ -136,9 +173,10 @@ class Time static [bool] GetShowSystemTrayDateTimeStatus() { $value = TryGetRegistryValue -Key $global:SysTrayPath -Property ([Time]::ShowSystemTrayDateTimeProperty) - if ($null -ne $value) + if (([string]::IsNullOrEmpty($value)) -or ($null -eq $value)) { - return $false + # if it is empty, we assume it is set to 1 + return $true } else { From 506a589a1948d934355b36f0f9df5f7c75f1bc31 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 5 Nov 2024 07:54:15 +0100 Subject: [PATCH 20/33] Include Pester tests --- .../Microsoft.Windows.Setting.Time/Time.md | 53 +++++++++ .../Microsoft.Windows.Setting.Time.psm1 | 111 +++++++++++++----- .../Microsoft.Windows.Setting.Time.Tests.ps1 | 75 ++++++++++++ 3 files changed, 207 insertions(+), 32 deletions(-) create mode 100644 resources/Help/Microsoft.Windows.Setting.Time/Time.md diff --git a/resources/Help/Microsoft.Windows.Setting.Time/Time.md b/resources/Help/Microsoft.Windows.Setting.Time/Time.md new file mode 100644 index 00000000..bbcaeb77 --- /dev/null +++ b/resources/Help/Microsoft.Windows.Setting.Time/Time.md @@ -0,0 +1,53 @@ +--- +external help file: Microsoft.Windows.Setting.Time.psm1-Help.xml +Module Name: Microsoft.Windows.Setting.Time +ms.date: 05/11/2024 +online version: +schema: 2.0.0 +title: Time +--- + +# Time + +## SYNOPSIS + +This `Time` DSC Resource allows you to manage the time zone, automatic time zone update, and system tray date/time visibility settings on a Windows machine. + +## DESCRIPTION + +This `Time` DSC Resource allows you to manage the time zone, automatic time zone update, and system tray date/time visibility settings on a Windows machine. + +## PARAMETERS + +| **Parameter** | **Attribute** | **DataType** | **Description** | **Allowed Values** | +| -------------------------- | ------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `TimeZone` | Key | String | Specifies the time zone to set on the machine. | Any valid time zone identifier from `Get-TimeZone -ListAvailable` | +| `SetTimeZoneAutomatically` | Optional | Boolean | The method to use to set the time zone automatically. The value should be either `NTP` or `NoSync`. The default value is `NTP`. | `NTP`, `NoSync` | +| `ShowSystemTrayClock` | Optional | Boolean | Whether to show the date and time in the system tray. The value should be a boolean. The default value is `$true`. | `$true`, `$false` | + + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Invoke-DscResource -Name Time -Method Set -Property @{ TimeZone = "Pacific Standard Time"; SetTimeZoneAutomatically = "NTP"; ShowSystemTrayDateTime = $true } + +# This example sets the time zone to Pacific Standard Time, sets the time zone to be updated automatically using NTP, and shows the date and time in the system tray. +``` + +### EXAMPLE 2 + +```powershell +Invoke-DscResource -Name Time -Method Get -Property {} + +# This example gets the current time settings on the machine. +``` + +### EXAMPLE 3 + +```powershell +Invoke-DscResource -Name Time -Method Test -Property @{ TimeZone = "Pacific Standard Time"} + +# This example tests whether the time zone is set to Pacific Standard Time. +``` diff --git a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 index 86a16757..d4732e70 100644 --- a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 +++ b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 @@ -38,43 +38,75 @@ enum TimeZoneAutomatically } #region Classes +<# +.SYNOPSIS + DSC Resource to manage Windows Time settings. + +.DESCRIPTION + This `Time` DSC Resource allows you to manage the time zone, automatic time zone update, and system tray date/time visibility settings on a Windows machine. + +.PARAMETER TimeZone + The time zone to set on the machine. The value should be a valid time zone ID from the list of time zones (Get-TimeZone -ListAvailable).Id. The default value is the current time zone. + +.PARAMETER SetTimeZoneAutomatically + The method to use to set the time zone automatically. The value should be either `NTP` or `NoSync`. The default value is `NTP`. + +.PARAMETER ShowSystemTrayDateTime + Whether to show the date and time in the system tray. The value should be a boolean. The default value is `$true`. + +.EXAMPLE + PS C:\> Invoke-DscResource -Name Time -Method Set -Property @{ TimeZone = "Pacific Standard Time"; SetTimeZoneAutomatically = "NTP"; ShowSystemTrayDateTime = $true } + + This example sets the time zone to Pacific Standard Time, sets the time zone to be updated automatically using NTP, and shows the date and time in the system tray. + +.EXAMPLE + PS C:\> Invoke-DscResource -Name Time -Method Get -Property {} + + This example gets the current time settings on the machine. + +.EXAMPLE + PS C:\> Invoke-DscResource -Name Time -Method Test -Property @{ TimeZone = "Pacific Standard Time"} + + This example tests whether the time zone is set to Pacific Standard Time. +#> [DscResource()] class Time { # Timezone values are taken from the list of timezones (Get-TimeZone -ListAvailable).Id + # TODO: Track issue 125 on PSDesiredStateConfiguration repository to add a ValidateSet for time zones [DscProperty(Key)] - [ValidateSet( - "Dateline Standard Time", "UTC-11", "Aleutian Standard Time", "Hawaiian Standard Time", "Marquesas Standard Time", - "Alaskan Standard Time", "UTC-09", "Pacific Standard Time (Mexico)", "UTC-08", "Pacific Standard Time", - "US Mountain Standard Time", "Mountain Standard Time (Mexico)", "Mountain Standard Time", "Central America Standard Time", - "Central Standard Time", "Easter Island Standard Time", "Central Standard Time (Mexico)", "Canada Central Standard Time", - "SA Pacific Standard Time", "Eastern Standard Time (Mexico)", "Eastern Standard Time", "Haiti Standard Time", - "Cuba Standard Time", "US Eastern Standard Time", "Turks And Caicos Standard Time", "Paraguay Standard Time", - "Atlantic Standard Time", "Venezuela Standard Time", "Central Brazilian Standard Time", "SA Western Standard Time", - "Pacific SA Standard Time", "Newfoundland Standard Time", "Tocantins Standard Time", "E. South America Standard Time", - "SA Eastern Standard Time", "Argentina Standard Time", "Greenland Standard Time", "Montevideo Standard Time", - "Magallanes Standard Time", "Saint Pierre Standard Time", "Bahia Standard Time", "UTC-02", "Mid-Atlantic Standard Time", - "Azores Standard Time", "Cape Verde Standard Time", "UTC", "Morocco Standard Time", "GMT Standard Time", - "Greenwich Standard Time", "W. Europe Standard Time", "Central Europe Standard Time", "Romance Standard Time", - "Central European Standard Time", "W. Central Africa Standard Time", "Namibia Standard Time", "Jordan Standard Time", - "GTB Standard Time", "Middle East Standard Time", "Egypt Standard Time", "E. Europe Standard Time", "Syria Standard Time", - "West Bank Standard Time", "South Africa Standard Time", "FLE Standard Time", "Israel Standard Time", "Kaliningrad Standard Time", - "Sudan Standard Time", "Libya Standard Time", "Namibia Standard Time", "Arabic Standard Time", "Turkey Standard Time", - "Arab Standard Time", "Belarus Standard Time", "Russian Standard Time", "E. Africa Standard Time", "Iran Standard Time", - "Arabian Standard Time", "Astrakhan Standard Time", "Azerbaijan Standard Time", "Russia Time Zone 3", "Mauritius Standard Time", - "Saratov Standard Time", "Georgian Standard Time", "Caucasus Standard Time", "Afghanistan Standard Time", "West Asia Standard Time", - "Ekaterinburg Standard Time", "Pakistan Standard Time", "India Standard Time", "Sri Lanka Standard Time", "Nepal Standard Time", - "Central Asia Standard Time", "Bangladesh Standard Time", "Omsk Standard Time", "Myanmar Standard Time", "SE Asia Standard Time", - "Altai Standard Time", "W. Mongolia Standard Time", "North Asia Standard Time", "N. Central Asia Standard Time", - "Tomsk Standard Time", "China Standard Time", "North Asia East Standard Time", "Singapore Standard Time", "W. Australia Standard Time", - "Taipei Standard Time", "Ulaanbaatar Standard Time", "North Korea Standard Time", "Aus Central W. Standard Time", - "Transbaikal Standard Time", "Tokyo Standard Time", "Korea Standard Time", "Yakutsk Standard Time", "Cen. Australia Standard Time", - "AUS Central Standard Time", "E. Australia Standard Time", "AUS Eastern Standard Time", "West Pacific Standard Time", - "Tasmania Standard Time", "Vladivostok Standard Time", "Lord Howe Standard Time", "Bougainville Standard Time", - "Russia Time Zone 10", "Magadan Standard Time", "Norfolk Standard Time", "Sakhalin Standard Time", "Central Pacific Standard Time", - "Russia Time Zone 11", "New Zealand Standard Time", "UTC+12", "Fiji Standard Time", "Kamchatka Standard Time", - "Chatham Islands Standard Time", "UTC+13", "Tonga Standard Time", "Samoa Standard Time", "Line Islands Standard Time" - )] + # [ValidateSet( + # "Dateline Standard Time", "UTC-11", "Aleutian Standard Time", "Hawaiian Standard Time", "Marquesas Standard Time", + # "Alaskan Standard Time", "UTC-09", "Pacific Standard Time (Mexico)", "UTC-08", "Pacific Standard Time", + # "US Mountain Standard Time", "Mountain Standard Time (Mexico)", "Mountain Standard Time", "Central America Standard Time", + # "Central Standard Time", "Easter Island Standard Time", "Central Standard Time (Mexico)", "Canada Central Standard Time", + # "SA Pacific Standard Time", "Eastern Standard Time (Mexico)", "Eastern Standard Time", "Haiti Standard Time", + # "Cuba Standard Time", "US Eastern Standard Time", "Turks And Caicos Standard Time", "Paraguay Standard Time", + # "Atlantic Standard Time", "Venezuela Standard Time", "Central Brazilian Standard Time", "SA Western Standard Time", + # "Pacific SA Standard Time", "Newfoundland Standard Time", "Tocantins Standard Time", "E. South America Standard Time", + # "SA Eastern Standard Time", "Argentina Standard Time", "Greenland Standard Time", "Montevideo Standard Time", + # "Magallanes Standard Time", "Saint Pierre Standard Time", "Bahia Standard Time", "UTC-02", "Mid-Atlantic Standard Time", + # "Azores Standard Time", "Cape Verde Standard Time", "UTC", "Morocco Standard Time", "GMT Standard Time", + # "Greenwich Standard Time", "W. Europe Standard Time", "Central Europe Standard Time", "Romance Standard Time", + # "Central European Standard Time", "W. Central Africa Standard Time", "Namibia Standard Time", "Jordan Standard Time", + # "GTB Standard Time", "Middle East Standard Time", "Egypt Standard Time", "E. Europe Standard Time", "Syria Standard Time", + # "West Bank Standard Time", "South Africa Standard Time", "FLE Standard Time", "Israel Standard Time", "Kaliningrad Standard Time", + # "Sudan Standard Time", "Libya Standard Time", "Namibia Standard Time", "Arabic Standard Time", "Turkey Standard Time", + # "Arab Standard Time", "Belarus Standard Time", "Russian Standard Time", "E. Africa Standard Time", "Iran Standard Time", + # "Arabian Standard Time", "Astrakhan Standard Time", "Azerbaijan Standard Time", "Russia Time Zone 3", "Mauritius Standard Time", + # "Saratov Standard Time", "Georgian Standard Time", "Caucasus Standard Time", "Afghanistan Standard Time", "West Asia Standard Time", + # "Ekaterinburg Standard Time", "Pakistan Standard Time", "India Standard Time", "Sri Lanka Standard Time", "Nepal Standard Time", + # "Central Asia Standard Time", "Bangladesh Standard Time", "Omsk Standard Time", "Myanmar Standard Time", "SE Asia Standard Time", + # "Altai Standard Time", "W. Mongolia Standard Time", "North Asia Standard Time", "N. Central Asia Standard Time", + # "Tomsk Standard Time", "China Standard Time", "North Asia East Standard Time", "Singapore Standard Time", "W. Australia Standard Time", + # "Taipei Standard Time", "Ulaanbaatar Standard Time", "North Korea Standard Time", "Aus Central W. Standard Time", + # "Transbaikal Standard Time", "Tokyo Standard Time", "Korea Standard Time", "Yakutsk Standard Time", "Cen. Australia Standard Time", + # "AUS Central Standard Time", "E. Australia Standard Time", "AUS Eastern Standard Time", "West Pacific Standard Time", + # "Tasmania Standard Time", "Vladivostok Standard Time", "Lord Howe Standard Time", "Bougainville Standard Time", + # "Russia Time Zone 10", "Magadan Standard Time", "Norfolk Standard Time", "Sakhalin Standard Time", "Central Pacific Standard Time", + # "Russia Time Zone 11", "New Zealand Standard Time", "UTC+12", "Fiji Standard Time", "Kamchatka Standard Time", + # "Chatham Islands Standard Time", "UTC+13", "Tonga Standard Time", "Samoa Standard Time", "Line Islands Standard Time" + # )] [string] $TimeZone = (Get-TimeZone).Id [DscProperty()] @@ -183,6 +215,21 @@ class Time return ($value -eq 1) } } + + # helper function for Pester tests + [hashtable] ToHashTable() + { + $parameters = @{} + foreach ($property in $this.PSObject.Properties) + { + if (-not ([string]::IsNullOrEmpty($property.Value))) + { + $parameters[$property.Name] = $property.Value + } + } + + return $parameters + } #endRegion Time helper functions } #endRegion Classes \ No newline at end of file diff --git a/tests/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.Tests.ps1 b/tests/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.Tests.ps1 index e69de29b..6f668d4b 100644 --- a/tests/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.Tests.ps1 +++ b/tests/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.Tests.ps1 @@ -0,0 +1,75 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +using module Microsoft.Windows.Setting.Time + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +<# +.Synopsis + Pester tests related to the Microsoft.Windows.Setting.Time PowerShell module. +#> + +BeforeAll { + if ($null -eq (Get-Module -ListAvailable -Name PSDesiredStateConfiguration)) + { + Install-Module -Name PSDesiredStateConfiguration -Force -SkipPublisherCheck + } + + $currentState = Invoke-DscResource -Name Time -ModuleName Microsoft.Windows.Setting.Time -Method Get -Property @{} + $global:Parameters = $currentState.ToHashTable() +} + +Describe 'List available DSC resources' { + It 'Shows DSC Resources' { + $expectedDSCResources = "Time" + $availableDSCResources = (Get-DscResource -Module Microsoft.Windows.Setting.Time).Name + $availableDSCResources.Count | Should -Be 1 + $availableDSCResources | Where-Object { $expectedDSCResources -notcontains $_ } | Should -BeNullOrEmpty -ErrorAction Stop + } +} + +Describe 'Time' { + It 'Display System Tray' { + $desiredState = @{ ShowSystemTrayDateTime = $true } + + Invoke-DscResource -Name Time -ModuleName Microsoft.Windows.Setting.Time -Method Set -Property $desiredState + + $finalState = Invoke-DscResource -Name Time -ModuleName Microsoft.Windows.Setting.Time -Method Test -Property $desiredState + $finalState.InDesiredState | Should -Be $true + } + + It 'Hide System Tray' { + $desiredState = @{ ShowSystemTrayDateTime = $false } + + Invoke-DscResource -Name Time -ModuleName Microsoft.Windows.Setting.Time -Method Set -Property $desiredState + + $finalState = Invoke-DscResource -Name Time -ModuleName Microsoft.Windows.Setting.Time -Method Test -Property @{} + $finalState.InDesiredState | Should -Be $true + } + + It 'Set Time Zone' { + $desiredState = @{ TimeZone = "Pacific Standard Time" } + + Invoke-DscResource -Name Time -ModuleName Microsoft.Windows.Setting.Time -Method Set -Property $desiredState + + $finalState = Invoke-DscResource -Name Time -ModuleName Microsoft.Windows.Setting.Time -Method Test -Property @{} + $finalState.InDesiredState | Should -Be $true + } + + It 'Set automatic updates to not synchronize' { + $object = [Time]::new() + $object.SetTimeZoneAutomatically = 'NoSync' + + # Set the state + $object.Set() + + # Test the state + $object.Test() | Should -Be $true + } +} +AfterAll { + # Restore the original state + Write-Host -Object ("Restoring the original state") + Invoke-DscResource -Name Time -ModuleName Microsoft.Windows.Setting.Time -Method Set -Property $global:Parameters +} \ No newline at end of file From 38a6184e711ea9767019d12c37f9969494d5357c Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 5 Nov 2024 08:41:03 +0100 Subject: [PATCH 21/33] Resolve remarks @Trenly --- .../Microsoft.Windows.Setting.Time.psm1 | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 index d4732e70..77a49d10 100644 --- a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 +++ b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 @@ -149,11 +149,7 @@ class Time if ($currentState.ShowSystemTrayDateTime -ne $this.ShowSystemTrayDateTime) { - $desiredState = switch ($this.ShowSystemTrayDateTime) - { - $true { "1" } # 1 = Show - $false { "0" } # 0 = Hide - } + $desiredState = [int]$this.ShowSystemTrayDateTime if ([string]::IsNullOrEmpty((TryGetRegistryValue -Key $global:SysTrayPath -Property ([Time]::ShowSystemTrayDateTimeProperty)))) { @@ -205,7 +201,7 @@ class Time static [bool] GetShowSystemTrayDateTimeStatus() { $value = TryGetRegistryValue -Key $global:SysTrayPath -Property ([Time]::ShowSystemTrayDateTimeProperty) - if (([string]::IsNullOrEmpty($value)) -or ($null -eq $value)) + if (([string]::IsNullOrEmpty($value))) { # if it is empty, we assume it is set to 1 return $true From e609482fcd66710c67ec74da1b167b5d4a7926ef Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 5 Nov 2024 09:37:07 +0100 Subject: [PATCH 22/33] Fix PSScriptAnalyzer rules --- utilities/scripts/New-DscResourceModule.ps1 | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/utilities/scripts/New-DscResourceModule.ps1 b/utilities/scripts/New-DscResourceModule.ps1 index 14f5a89f..24294f70 100644 --- a/utilities/scripts/New-DscResourceModule.ps1 +++ b/utilities/scripts/New-DscResourceModule.ps1 @@ -5,7 +5,7 @@ function New-DscResourceModule Creates a new DSC (Desired State Configuration) resource module structure. .DESCRIPTION - The function New-DscResourceModule function creates a new DSC resource module structure with the specified name and description. + The function New-DscResourceModule function creates a new DSC resource module structure with the specified name and description. It sets up the necessary directory structure for resources and tests within the given base path. .PARAMETER DscResourceModule @@ -22,7 +22,8 @@ function New-DscResourceModule This command creates a new DSC resource module named 'Microsoft.Windows.Language' with the specified description in the default base path. #> - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPositionalParameters", "", Justification = "Positional parameters are used for simplicity. Targeting PS 7+")] param ( [Parameter(Mandatory)] [string]$DscResourceModule, @@ -67,9 +68,11 @@ function New-DscResourceModule if (-not (Test-Path $moduleManifestPath)) { - Write-Verbose -Message ("Creating module manifest in: $moduleManifestPath with") - Write-Verbose -Message ($moduleManifestParams | ConvertTo-Json -Depth 10 | Out-String) - New-ModuleManifest @moduleManifestParams + if ($PSCmdlet.ShouldProcess($moduleManifestPath, 'Create module manifest')) + { + Write-Verbose -Message ($moduleManifestParams | ConvertTo-Json -Depth 10 | Out-String) + New-ModuleManifest @moduleManifestParams + } # Workaround for issue: https://github.com/PowerShell/PowerShell/issues/5922 $fileContent = Get-Content $moduleManifestPath @@ -80,14 +83,14 @@ function New-DscResourceModule $newPrerelease = "Prerelease = 'alpha'" $fileContent = $fileContent -replace '# Prerelease = ''''', $newPrerelease # TODO: Add tags - + Set-Content -Path $moduleManifestPath -Value $fileContent } $psm1Path = Join-Path -Path $resourcePath -ChildPath "$DscResourceModule.psm1" if (-not (Test-Path $psm1Path)) { - $null = New-Item -ItemType File -Path $psm1Path -Force + $null = New-Item -ItemType File -Path $psm1Path -Force } $testsFilePath = Join-Path -Path $testsPath -ChildPath "$DscResourceModule.Tests.ps1" From 48877f6141117d099f3f48d82b8a47f7bdb8c696 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 5 Nov 2024 17:07:11 +0100 Subject: [PATCH 23/33] Comments --- .../Microsoft.Windows.Setting.WindowsUpdate.psd1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psd1 b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psd1 index 7636d788..d044afd4 100644 --- a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psd1 +++ b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psd1 @@ -69,16 +69,16 @@ # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = '*' + # FunctionsToExport = '*' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = '*' + # CmdletsToExport = '*' # Variables to export from this module - VariablesToExport = '*' + # VariablesToExport = '*' # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = '*' + # AliasesToExport = '*' # DSC resources to export from this module DscResourcesToExport = @('WindowsUpdate') From 41914ef43ad83aae8afbc3f4a0fe9783ad75489b Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Tue, 5 Nov 2024 17:28:20 +0100 Subject: [PATCH 24/33] Resolve comments from Trenly --- .../Microsoft.Windows.Setting.Time.psd1 | 8 ++++---- .../Microsoft.Windows.Setting.Time.psm1 | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 index 6d1223a2..048b6ae9 100644 --- a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 +++ b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psd1 @@ -69,16 +69,16 @@ # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = '*' + # FunctionsToExport = '*' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = '*' + # CmdletsToExport = '*' # Variables to export from this module - VariablesToExport = '*' + # VariablesToExport = '*' # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = '*' + # AliasesToExport = '*' # DSC resources to export from this module DscResourcesToExport = @('Time') diff --git a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 index 77a49d10..a6747c33 100644 --- a/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 +++ b/resources/Microsoft.Windows.Setting.Time/Microsoft.Windows.Setting.Time.psm1 @@ -147,7 +147,7 @@ class Time Set-TimeZone -Id $this.TimeZone } - if ($currentState.ShowSystemTrayDateTime -ne $this.ShowSystemTrayDateTime) + if (($null -ne $this.ShowSystemTrayDateTime) -and ($currentState.ShowSystemTrayDateTime -ne $this.ShowSystemTrayDateTime)) { $desiredState = [int]$this.ShowSystemTrayDateTime From d816dc3cc0e5b82f9c2ea7e42f48e7f10c473261 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 6 Nov 2024 02:56:12 +0100 Subject: [PATCH 25/33] Re-add Invoke-Process --- resources/PythonPip3Dsc/PythonPip3Dsc.psm1 | 59 +++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 b/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 index 6a0f8053..0f900faf 100644 --- a/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 +++ b/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 @@ -4,6 +4,61 @@ using namespace System.Collections.Generic #region Functions +function Invoke-Process { + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string]$FilePath, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string]$ArgumentList + ) + + try { + $pinfo = New-Object System.Diagnostics.ProcessStartInfo + $pinfo.FileName = $FilePath + $pinfo.RedirectStandardError = $true + $pinfo.RedirectStandardOutput = $true + $pinfo.UseShellExecute = $false + $pinfo.WindowStyle = 'Hidden' + $pinfo.CreateNoWindow = $true + $pinfo.Arguments = $ArgumentList + $p = New-Object System.Diagnostics.Process + $p.StartInfo = $pinfo + $p.Start() | Out-Null + + $stOut = @() + # using ReadLine() instead of ReadToEnd() for building array object. ReadToEnd() gave different output than ReadLine() in some cases. + while (-not $p.StandardOutput.EndOfStream) { + $stOut += $p.StandardOutput.ReadLine() + } + + $stErr = @() + while (-not $p.StandardError.EndOfStream) { + $stErr += $p.StandardError.ReadLine() + } + + $result = [pscustomobject]@{ + Title = ($MyInvocation.MyCommand).Name + Command = $FilePath + Arguments = $ArgumentList + StdOut = $stOut + StdErr = $stErr + ExitCode = $p.ExitCode + } + + $p.WaitForExit() + + return $result + } catch { + Write-Verbose -Message "Error occurred while executing the command: $FilePath $ArgumentList. Error:" + Write-Verbose -Message $stErr + } +} + function Get-Pip3Path { if ($IsWindows) { # Note: When installing 64-bit version, the registry key: HKLM:\SOFTWARE\Wow6432Node\Python\PythonCore\*\InstallPath was empty. @@ -226,9 +281,9 @@ function Invoke-Pip3 { ) if ($global:usePip3Exe) { - return Start-Process -FilePath $global:pip3ExePath -ArgumentList $command -Wait -PassThru -WindowStyle Hidden + return Invoke-Process -FilePath $global:pip3ExePath -ArgumentList $command } else { - return Start-Process -FilePath pip3 -ArgumentList $command -Wait -PassThru -WindowStyle hidden + return Invoke-Process -FilePath pip3 -ArgumentList $command } } From cd15ed2399edc0720b9f92ed8bfaf9f586f1fc2f Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 6 Nov 2024 02:59:21 +0100 Subject: [PATCH 26/33] Fix spelling --- .github/actions/spelling/allow.txt | 3 +++ resources/PythonPip3Dsc/PythonPip3Dsc.psm1 | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index b5196fda..ed7635cc 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -37,3 +37,6 @@ uilt Windo ELSPROBLEMS requ +whatif +pscustomobject +itsdangerouss \ No newline at end of file diff --git a/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 b/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 index 0f900faf..f9c22099 100644 --- a/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 +++ b/resources/PythonPip3Dsc/PythonPip3Dsc.psm1 @@ -205,7 +205,7 @@ function Invoke-Pip3Uninstall { $command.Add((Get-PackageNameWithVersion @PSBoundParameters)) $command.Add($Arguments) - # '--yes' is needed to ignore confrimation required for uninstalls + # '--yes' is needed to ignore conformation required for uninstalls $command.Add('--yes') return Invoke-Pip3 -command $command } From 6370252f94c386829730170a24680849545c46f6 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 6 Nov 2024 03:02:02 +0100 Subject: [PATCH 27/33] Spell checker test --- .github/actions/spelling/allow.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index ed7635cc..7cc76280 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -39,4 +39,5 @@ ELSPROBLEMS requ whatif pscustomobject +itsdangerouss itsdangerouss \ No newline at end of file From 7ed2c35be6b4dd7a3dceebdde8f511e35d16446f Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 6 Nov 2024 03:07:53 +0100 Subject: [PATCH 28/33] Add attributes for validation --- ...crosoft.Windows.Setting.WindowsUpdate.psm1 | 156 +++++------------- 1 file changed, 37 insertions(+), 119 deletions(-) diff --git a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 index f26d1e87..375ee96a 100644 --- a/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 +++ b/resources/Microsoft.Windows.Setting.WindowsUpdate/Microsoft.Windows.Setting.WindowsUpdate.psm1 @@ -5,8 +5,7 @@ $global:WindowsUpdateSettingPath = 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Se $global:DeliveryOptimizationSettingPath = 'Registry::HKEY_USERS\S-1-5-20\Software\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Settings' #region Functions -function DoesRegistryKeyPropertyExist -{ +function DoesRegistryKeyPropertyExist { param ( [Parameter(Mandatory)] [string]$Path, @@ -20,8 +19,7 @@ function DoesRegistryKeyPropertyExist return $null -ne $itemProperty } -function Test-WindowsUpdateRegistryKey -{ +function Test-WindowsUpdateRegistryKey { param ( [Parameter(Mandatory)] [hashtable] $RegistryKeyProperty, @@ -31,11 +29,9 @@ function Test-WindowsUpdateRegistryKey ) $result = $true - foreach ($key in $RegistryKeyProperty.Keys) - { + foreach ($key in $RegistryKeyProperty.Keys) { $value = $RegistryKeyProperty[$key] - if ($value -ne $CurrentState.$key) - { + if ($value -ne $CurrentState.$key) { $result = $false } } @@ -43,8 +39,7 @@ function Test-WindowsUpdateRegistryKey return $result } -function Set-WindowsUpdateRegistryKey -{ +function Set-WindowsUpdateRegistryKey { param ( [Parameter(Mandatory)] [string]$Path, @@ -54,32 +49,19 @@ function Set-WindowsUpdateRegistryKey [hashtable] $RegistryKeyProperty ) - if (-not (Test-Path -Path $Path)) - { + if (-not (Test-Path -Path $Path)) { $null = New-Item -Path $Path -Force } - foreach ($key in $RegistryKeyProperty.Keys) - { + foreach ($key in $RegistryKeyProperty.Keys) { $value = $RegistryKeyProperty[$key] $typeInfo = $value.GetType().Name - if ($typeInfo -eq 'Boolean') - { + if ($typeInfo -eq 'Boolean') { $value = [int]$value } - # validate the value of UserChoiceActiveHoursEnd and UserChoiceActiveHoursStart to be between 0 and 24 - Assert-UserChoiceValue -KeyName $key -Value $value - - # validate the value of DownloadRateBackgroundPct, DownloadRateForegroundPct and UpRatePctBandwith to be between 0 and 100 - Assert-RatePercentageValue -KeyName $key -Value $value - - # validate the value of UpRatePctBandwith to be between 5 and 500 - Assert-UpRateValue -KeyName $key -Value $value - - if (-not (DoesRegistryKeyPropertyExist -Path $Path -Name $key)) - { + if (-not (DoesRegistryKeyPropertyExist -Path $Path -Name $key)) { $null = New-ItemProperty -Path $Path -Name $key -Value $value -PropertyType 'DWord' -Force } @@ -88,92 +70,35 @@ function Set-WindowsUpdateRegistryKey } } -function Assert-UpRateValue -{ - param ( - [Parameter(Mandatory)] - [string] $KeyName, - - [Parameter(Mandatory)] - [int] $Value - ) - - if ($KeyName -eq 'UpRatePctBandwidth' -and $Value -notin (5..500)) - { - Throw "You are specifying a percentage value, which must be between 5 and 500. The value you provided is $Value. Please provide a value between 5 and 500." - } -} - -function Assert-RatePercentageValue -{ - param ( - [Parameter(Mandatory)] - [string] $KeyName, - - [Parameter(Mandatory)] - [int] $Value - ) - - if ($KeyName -in ('DownloadRateBackgroundPct', 'DownloadRateForegroundPct', 'UpRatePctBandwidth') -and $Value -notin (0..100)) - { - # TODO: It might be beneficial to add `Reasons` and not throw, only return statement - Throw "You are specifying a percentage value, which must be between 0 and 100. The value you provided is $Value. Please provide a value between 0 and 100." - } -} - -function Assert-UserChoiceValue -{ - param ( - [Parameter(Mandatory)] - [string] $KeyName, - - [Parameter(Mandatory)] - [int] $Value - ) - - if ($KeyName -in ('UserChoiceActiveHoursEnd', 'UserChoiceActiveHoursStart') -and $Value -notin (0..24)) - { - Throw "Value must be between 0 and 24" - } -} - -function Assert-DownloadRate -{ +function Assert-DownloadRate { param ( [Parameter(Mandatory)] [hashtable] $Parameters ) - if ($Parameters.ContainsKey('DownloadRateBackgroundPct') -or $Parameters.ContainsKey('DownloadRateForegroundPct')) - { - if ($Parameters.ContainsKey('DownloadRateBackgroundBps') -or $Parameters.ContainsKey('DownloadRateForegroundBps')) - { - Throw "Cannot set both DownloadRateBackgroundPct/DownloadRateForegroundPct and DownloadRateBackgroundBps/DownloadRateForegroundBps" + if ($Parameters.ContainsKey('DownloadRateBackgroundPct') -or $Parameters.ContainsKey('DownloadRateForegroundPct')) { + if ($Parameters.ContainsKey('DownloadRateBackgroundBps') -or $Parameters.ContainsKey('DownloadRateForegroundBps')) { + Throw 'Cannot set both DownloadRateBackgroundPct/DownloadRateForegroundPct and DownloadRateBackgroundBps/DownloadRateForegroundBps' } } } -function Initialize-WindowsUpdate -{ +function Initialize-WindowsUpdate { $class = [WindowsUpdate]::new() $hiddenProperties = $class | Get-Member -Static -Force | Where-Object { $_.MemberType -eq 'Property' } | Select-Object -ExpandProperty Name - foreach ($p in $hiddenProperties) - { - $classPropertyName = $p.Replace("Property", "") + foreach ($p in $hiddenProperties) { + $classPropertyName = $p.Replace('Property', '') $dataType = $class | Get-Member | Where-Object { $_.Name -eq $classPropertyName } | Select-Object -ExpandProperty Definition | Select-String -Pattern '\[.*\]' | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty Value $currentValue = [WindowsUpdate]::GetRegistryValue($class::$p) - if ($null -eq $currentValue) - { - if ($dataType -eq '[bool]') - { + if ($null -eq $currentValue) { + if ($dataType -eq '[bool]') { $currentValue = $false } - if ($dataType -eq '[int]') - { + if ($dataType -eq '[int]') { $currentValue = 0 } } @@ -244,8 +169,7 @@ function Initialize-WindowsUpdate This command gets the current Windows Update settings. #> [DSCResource()] -class WindowsUpdate -{ +class WindowsUpdate { # Key required. Do not set. [DscProperty(Key)] [string] $SID @@ -269,9 +193,11 @@ class WindowsUpdate [nullable[bool]] $SmartActiveHoursState [DscProperty()] + [ValidateRange(0, 24)] [nullable[int]] $UserChoiceActiveHoursEnd [DscProperty()] + [ValidateRange(0, 24)] [nullable[int]] $UserChoiceActiveHoursStart [DscProperty()] @@ -285,15 +211,19 @@ class WindowsUpdate [nullable[int]] $DownloadRateForegroundBps [DscProperty()] + [ValidateRange(0, 100)] [nullable[int]] $DownloadRateBackgroundPct [DscProperty()] + [ValidateRange(0, 100)] [nullable[int]] $DownloadRateForegroundPct [DscProperty()] + [ValidateRange(5, 500)] [nullable[int]] $UploadLimitGBMonth [DscProperty()] + [ValidateRange(0, 100)] [nullable[int]] $UpRatePctBandwidth static hidden [string] $IsContinuousInnovationOptedInProperty = 'IsContinuousInnovationOptedIn' @@ -312,24 +242,20 @@ class WindowsUpdate static hidden [string] $UploadLimitGBMonthProperty = 'UploadLimitGBMonth' static hidden [string] $UpRatePctBandwidthProperty = 'UpRatePctBandwidth' - [WindowsUpdate] Get() - { + [WindowsUpdate] Get() { $currentState = Initialize-WindowsUpdate return $currentState } - [bool] Test() - { + [bool] Test() { $currentState = $this.Get() $settableProperties = $this.GetParameters() return (Test-WindowsUpdateRegistryKey -RegistryKeyProperty $settableProperties -CurrentState $currentState) } - [void] Set() - { - if ($this.Test()) - { + [void] Set() { + if ($this.Test()) { return } @@ -341,17 +267,12 @@ class WindowsUpdate } #region WindowsUpdate helper functions - static [object] GetRegistryValue($PropertyName) - { + static [object] GetRegistryValue($PropertyName) { $value = $null - if ($null -ne $PropertyName) - { - if ((DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name $PropertyName)) - { + if ($null -ne $PropertyName) { + if ((DoesRegistryKeyPropertyExist -Path $global:WindowsUpdateSettingPath -Name $PropertyName)) { $value = Get-ItemProperty -Path $global:WindowsUpdateSettingPath -Name $PropertyName | Select-Object -ExpandProperty $PropertyName - } - elseif ((DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name $PropertyName)) - { + } elseif ((DoesRegistryKeyPropertyExist -Path $global:DeliveryOptimizationSettingPath -Name $PropertyName)) { $value = Get-ItemProperty -Path $global:DeliveryOptimizationSettingPath -Name $PropertyName | Select-Object -ExpandProperty $PropertyName } } @@ -359,13 +280,10 @@ class WindowsUpdate return $value } - [hashtable] GetParameters() - { + [hashtable] GetParameters() { $parameters = @{} - foreach ($property in $this.PSObject.Properties) - { - if (-not ([string]::IsNullOrEmpty($property.Value))) - { + foreach ($property in $this.PSObject.Properties) { + if (-not ([string]::IsNullOrEmpty($property.Value))) { $parameters[$property.Name] = $property.Value } } From 6dfddf3106f3f8706a150f1499d8fc99c7a85d02 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 6 Nov 2024 03:09:24 +0100 Subject: [PATCH 29/33] Spell checker words --- .github/actions/spelling/allow.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 7cc76280..d3f605b8 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -40,4 +40,7 @@ requ whatif pscustomobject itsdangerouss -itsdangerouss \ No newline at end of file +itsdangerouss +Bandwith +PCs +wmiprvse \ No newline at end of file From 8d8ad1207719234e34173ec80ab27e7b2d2ff824 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 6 Nov 2024 03:11:09 +0100 Subject: [PATCH 30/33] Test --- .github/actions/spelling/allow.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index d3f605b8..3008b89c 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -43,4 +43,4 @@ itsdangerouss itsdangerouss Bandwith PCs -wmiprvse \ No newline at end of file +wmiprvse From 1fbe8e21c915faa40d56570328fed4c364fbc38c Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 6 Nov 2024 03:12:24 +0100 Subject: [PATCH 31/33] Add line break for test --- .github/actions/spelling/allow.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 3008b89c..ed8ea9bc 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -44,3 +44,4 @@ itsdangerouss Bandwith PCs wmiprvse + From a00ec3e6d1e9ceb1ac15502c98c98b864f65d5e9 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 6 Nov 2024 03:17:24 +0100 Subject: [PATCH 32/33] Fix spelling --- .github/actions/spelling/allow.txt | 2 +- .../Microsoft.Windows.Setting.Language.psm1 | 117 ++++++------------ 2 files changed, 42 insertions(+), 77 deletions(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index ed8ea9bc..6d73adb3 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -44,4 +44,4 @@ itsdangerouss Bandwith PCs wmiprvse - +Workaround diff --git a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 index 1e5652b2..fc2ea0f6 100644 --- a/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 +++ b/resources/Microsoft.Windows.Setting.Language/Microsoft.Windows.Setting.Language.psm1 @@ -3,17 +3,15 @@ using namespace System.Collections.Generic -$global:LocaleNameRegistryPath = "HKCU:\Control Panel\International" -$global:LocaleUserProfilePath = "HKCU:\Control Panel\International\User Profile" +$global:LocaleNameRegistryPath = 'HKCU:\Control Panel\International' +$global:LocaleUserProfilePath = 'HKCU:\Control Panel\International\User Profile' #region Functions -function Get-OsBuildVersion -{ +function Get-OsBuildVersion { return [System.Environment]::OSVersion.Version.Build } -function TryGetRegistryValue -{ +function TryGetRegistryValue { param ( [Parameter(Mandatory = $true)] [string]$Key, @@ -22,43 +20,33 @@ function TryGetRegistryValue [string]$Property ) - if (Test-Path -Path $Key) - { - try - { + if (Test-Path -Path $Key) { + try { return (Get-ItemProperty -Path $Key | Select-Object -ExpandProperty $Property) - } - catch - { + } catch { Write-Verbose "Property `"$($Property)`" could not be found." } - } - else - { - Write-Verbose "Registry key does not exist." + } else { + Write-Verbose 'Registry key does not exist.' } } -function Get-LocaleList -{ +function Get-LocaleList { $localeList = Get-WinUserLanguageList $out = [List[Language]]::new() - foreach ($locale in $localeList) - { - $langague = [Language]::new($locale.LanguageTag, $true) - $out.Add($langague) + foreach ($locale in $localeList) { + $language = [Language]::new($locale.LanguageTag, $true) + $out.Add($language) } # section to include other languages that can be installed # helpful for users to discover what packages can be installed - $allLangues = [System.Globalization.CultureInfo]::GetCultures("AllCultures") - foreach ($culture in $allLangues) - { - if ($out.LocaleName -notcontains $culture.Name -and -not ([string]::IsNullOrEmpty($culture.Name))) - { - $langague = [Language]::new($culture.Name, $false) - $out.Add($langague) + $allLangues = [System.Globalization.CultureInfo]::GetCultures('AllCultures') + foreach ($culture in $allLangues) { + if ($out.LocaleName -notcontains $culture.Name -and -not ([string]::IsNullOrEmpty($culture.Name))) { + $language = [Language]::new($culture.Name, $false) + $out.Add($language) } } @@ -84,8 +72,7 @@ function Get-LocaleList This example installs the English (United States) language on the local machine. #> [DscResource()] -class Language -{ +class Language { [DscProperty(Key)] [string] $LocaleName @@ -94,73 +81,59 @@ class Language static [hashtable] $InstalledLocality - Language() - { + Language() { [Language]::GetInstalledLocality() } - Language([string] $LocaleName, [bool] $Exist) - { + Language([string] $LocaleName, [bool] $Exist) { $this.LocaleName = $LocaleName $this.Exist = $Exist } - [Language] Get() - { + [Language] Get() { $keyExist = [Language]::InstalledLocality.ContainsKey(($this.LocaleName)) $currentState = [Language]::InstalledLocality[$this.LocaleName] - if (-not $keyExist) - { + if (-not $keyExist) { return [Language]::new($this.LocaleName, $false) } return $currentState } - [void] Set() - { - if ($this.Test()) - { + [void] Set() { + if ($this.Test()) { return } - if ($this.Exist) - { + if ($this.Exist) { # use the LanguagePackManagement module to install the language (requires elevation). International does not have a cmdlet to install language Install-Language -Language $this.LocaleName - } - else - { + } else { Uninstall-Language -Language $this.LocaleName } } - [bool] Test() - { + [bool] Test() { $currentState = $this.Get() - if ($currentState.Exist -ne $this.Exist) - { + if ($currentState.Exist -ne $this.Exist) { return $false } return $true } - static [Language[]] Export() - { + static [Language[]] Export() { return Get-LocaleList } #region Language helper functions - static [void] GetInstalledLocality() - { + static [void] GetInstalledLocality() { [Language]::InstalledLocality = @{} - foreach ($locality in [Language]::Export()) - { + foreach ($locality in [Language]::Export()) { [Language]::InstalledLocality[$locality.LocaleName] = $locality } } @@ -183,8 +156,7 @@ class Language This example sets the display language to English (United States) on the user. #> [DscResource()] -class DisplayLanguage -{ +class DisplayLanguage { [DscProperty(Key)] [string] $LocaleName @@ -194,18 +166,15 @@ class DisplayLanguage hidden [string] $KeyName = 'LocaleName' - DisplayLanguage() - { + DisplayLanguage() { } - [DisplayLanguage] Get() - { + [DisplayLanguage] Get() { $currentState = [DisplayLanguage]::new() # check if user profile contains display language - $userProfileLanguageDict = TryGetRegistryValue -Key (Join-path $global:LocaleUserProfilePath $this.LocaleName) -Property 'CachedLanguageName' - if ((TryGetRegistryValue -Key $global:LocaleNameRegistryPath -Property $this.KeyName) -ne $this.LocaleName -and ($null -ne $userProfileLanguageDict)) - { + $userProfileLanguageDict = TryGetRegistryValue -Key (Join-Path $global:LocaleUserProfilePath $this.LocaleName) -Property 'CachedLanguageName' + if ((TryGetRegistryValue -Key $global:LocaleNameRegistryPath -Property $this.KeyName) -ne $this.LocaleName -and ($null -ne $userProfileLanguageDict)) { $currentState.Exist = $false return $currentState } @@ -216,10 +185,8 @@ class DisplayLanguage } } - [void] Set() - { - if ($this.Test()) - { + [void] Set() { + if ($this.Test()) { return } @@ -229,12 +196,10 @@ class DisplayLanguage # TODO: Exist does not make sense here, we always want a language to exist } - [bool] Test() - { + [bool] Test() { $currentState = $this.Get() - if ($currentState.Exist -ne $this.Exist) - { + if ($currentState.Exist -ne $this.Exist) { return $false } From 35badfd1928803cd6e561dc4cd319ae48c26e93f Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 6 Nov 2024 03:19:30 +0100 Subject: [PATCH 33/33] Temporary add names to pass spell checker --- .github/actions/spelling/allow.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 6d73adb3..320c4d8c 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -45,3 +45,11 @@ Bandwith PCs wmiprvse Workaround +Belarus +FLE +GTB +Marquesas +Myanmar +Systray +timezones +Ulaanbaatar