helpers.ps1
function Write-Logs { [CmdletBinding()] [OutputType([boolean])] param ( # Text which will be written into the eventlog [Parameter( Mandatory = $true )] [string[]] [Alias("text")] $Message, # LogLevel [Parameter( Mandatory = $false )] [ValidateSet( "Debug", "Info","Information","Informational", "Notice", "Warning", "Error", "Critical", "Alert", "Emergency" )] [Alias("Level","Severity")] [string] $LogLevel = "informational", [Parameter(Mandatory = $false)] [ValidateSet("eventlog","console","file")] [String[]]$LogTypes = @("console","file"), [Parameter(Mandatory = $false)] [String]$LogFile = (Join-Path $env:APPDATA "psChocoUpdateNotify\psChocoUpdateNotify.log") ) process { foreach ($logType in $LogTypes) { switch ($logType) { "eventlog" { # Windows eventlog does not know about all LogLevels from syslog if ( @("Debug","Informational","Notice") -contains $LogLevel) { $LogLevel = "Information" } if ( @("Critical","Emergency") -contains $LogLevel) { $LogLevel = "Error" } foreach ($logEntry in $Message) { Write-EventLog -Logname "Application" -Source "psChocoUpdateNotifier" -EventId 1 -EntryType $LogLevel -Message $logEntry } } "console" { foreach ($logEntry in $Message) { Write-Host "$($LogLevel.ToUpper()): $($logEntry)" } } "file" { if(!(Test-Path $LogFile)) { New-Item -Path $LogFile -Force | Out-Null } foreach ($logEntry in $Message) { Out-File -FilePath $LogFile -Encoding utf8 -Append -InputObject "$($LogLevel.ToUpper()): $($logEntry)" } } Default {} } } } } function Start-Choco { # implement the pschoco module from https://gitlab.com/Paxz/choco_gui/tree/master/pschoco into this script, for better integration <# .SYNOPSIS Chocolatey Output Parserfunction .DESCRIPTION This function behaves like the normal choco.exe, except that it interepretes the given results of some commands and parses them to PSCustomObjects. This should make working with chocolatey alot easier if you really want to integrate it into your scripts. .PARAMETER command Chocolatey Command - basically the same command you would write after `choco`. Original Documentation to Chocolatey Commands: https://github.com/chocolatey/choco/wiki/CommandsList .PARAMETER options Chocolatey Options - the same options that you would write after the command of an `choco`-Invoke Original Documentation to Chocolatey Options and Switches: https://github.com/chocolatey/choco/wiki/CommandsReference#default-options-and-switches .INPUTS Options can be given through the pipeline. Further explained in Example 4. .OUTPUTS [System.Management.Automation.PSCustomObject], PSCustomObject of all important informations returned by the `choco` call .EXAMPLE PS C:\>Start-Choco -Command "list" -Option "-lo" Runs `choco list -lo` and parses the output to an object with the Attributes `PackageName` and `Version`. The options parameter has to be written in `"` or `'` so that powershell doesn't interpret the Value as an extra Parameter for this function .EXAMPLE PS C:\>Start-Choco info vscode Runs `choco info vscode` and parses the output to an PSCustomObject .EXAMPLE PS C:\>pschoco outdated Runs `choco outdated` over the function alias and parses the output like explained in the first example. .EXAMPLE PS C:\>@("vscode","firefox") | Start-Choco info Options can be passed through the pipeline. Thisway each entry will be given as the option: `Start-Choco info <PipeElement>`. .LINK https://github.com/chocolatey/choco/wiki/CommandsList https://github.com/chocolatey/choco/wiki/CommandsReference#default-options-and-switches .NOTES Currently Supported Chocolatey Commands (everything else works like the default `choco.exe`): - outdated - search|list|find - source|sources - info - config - feature - pin #> [CmdletBinding()] [alias("schoco","pschoco")] param ( [Parameter( Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=0 )] [ValidateNotNullOrEmpty()] [string] $command, [Parameter( Mandatory=$false, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, Position=1 )] [string[]] $options = @() ) begin { $proc = $null try { $proc = Start-Process -FilePath "roco" -Wait -PassThru -NoNewWindow -ErrorAction SilentlyContinue } catch { } if ($null -eq $proc -or $proc.ExitCode -ne 0) { $ChocoEXE = "choco" } else { $ChocoEXE = "roco" } Write-Host "Using $ChocoEXE" } process { switch -Regex ($command) { '^(outdated)$' { & $ChocoEXE $command @options | Select-String -Pattern '^([\w-.]+)\|(.*)\|(.*)\|.*$' | ForEach-Object { [PSCustomObject]@{ PackageName = $_.matches.groups[1].value currentVersion = $_.matches.groups[2].value newVersion = $_.matches.groups[3].value } } } '^(search|list|find)$' { & $ChocoEXE $command @options | Select-String -Pattern '^([\w-.]+) ([\d.]+)' | ForEach-Object { [PSCustomObject]@{ PackageName = $_.matches.groups[1].value Version = $_.matches.groups[2].value } } } '^(source[s]*)$' { if($options -notcontains 'add|disable|enable|remove') { & $ChocoEXE $command @options | Select-String -Pattern '^([\w-.]+)( \[Disabled\])? - (\S+) \| Priority (\d)\|Bypass Proxy - (\w+)\|Self-Service - (\w+)\|Admin Only - (\w+)\.$' | ForEach-Object { if ($_.matches.groups[2].value -eq ' [Disabled]') { $Enabled = $False } else { $Enabled = $True } [PSCustomObject]@{ SourceName = $_.matches.groups[1].value Enabled = $Enabled Url = $_.matches.groups[3].value Priority = $_.matches.groups[4].value "Bypass Proxy" = $_.matches.groups[5].value "Self-Service" = $_.matches.groups[6].value "Admin Only" = $_.matches.groups[7].value } } } else { & $ChocoEXE $command @options } } '^(info)$' { $infoArray = (((& $ChocoEXE $command @options) -split '\|') | Where-Object {$_ -match '.*: .*'}).trim() -replace ': ','=' | ConvertFrom-StringData $infoReturn = New-Object PSObject foreach ($infoItem in $infoArray) { Add-Member -InputObject $infoReturn -MemberType NoteProperty -Name $infoItem.Keys -Value ($infoItem.Values -as [string]) } return $infoReturn } '^(config)$' { if($options -notcontains 'get|set|unset') { $chocoResult = & $ChocoEXE $command @options $Settings = foreach ($line in $chocoResult) { Select-String -InputObject $line -Pattern "^(\w+) = (\w+|) \|.*"| ForEach-Object { [PSCustomObject]@{ "Setting" = $_.matches.groups[1].value "Value" = $_.matches.groups[2].value } } } $Features = foreach ($line in $chocoResult) { Select-String -InputObject $line -Pattern "\[([x ])\] (\w+).*" | ForEach-Object { if($_.matches.groups[1].value -eq "x") { $value = $true } else { $value = $false } [PSCustomObject]@{ "Setting" = $_.matches.groups[2].value "Enabled" = $value } } } return [PSCustomObject]@{ Settings = $Settings Features = $Features } } else { & $ChocoEXE $command $options } } '^(feature[s]*)$' { if($options -notcontains 'disable|enable') { & $ChocoEXE $command @options | Select-String -Pattern '\[([x ])\] (\w+).*' | ForEach-Object { if($_.matches.groups[1].value -eq "x") { $value = $true } else { $value = $false } [PSCustomObject]@{ "Setting" = $_.matches.groups[2].value "Enabled" = $value } } } } '^(pin)$' { if($options -notcontains 'add|remove') { # options enthält nicht add oder remove & $ChocoEXE $command @options | Select-String -Pattern '^(.+)\|(.+)' | ForEach-Object { [PSCustomObject]@{ packageName = $_.matches.groups[1].value pinnedVersion = $_.matches.groups[2].value } } } else { & $ChocoEXE $command @options } } Default { & $ChocoEXE $command @options } } } end { } } function Get-UpdateInfo { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyCollection()] [Object[]]$dtUpdates ) $dtUpdates = @($dtUpdates) $SelectedCount = ($dtUpdates | Group-Object -AsHashTable -Property doUpdate).True.Count $SelectedText = "" if ($dtUpdates.Count -eq 1) { $UpdateCountText = "Update" } else { $UpdateCountText = "Updates" } $SelectedText = "($SelectedCount selected for update)" if ($dtUpdates.Count -le 0) { "No updates available :>" } else { "$($dtUpdates.Count) $UpdateCountText available $SelectedText; Double-Click a package for more info" } } function Update-PackageList { $script:uiHash.currentAction = "Search" Show-Overlay -Text "Searching for updates" $dtUpdates.Clear() $script:uiHash.window = $window $script:uiHash.gOverlay = $gOverlay $script:uiHash.spOverlay = $spOverlay $script:uiHash.tbOverlay = $tbOverlay $script:uiHash.dgUpdates = $dgUpdates $script:uiHash.tbOverlayProgress = $tbOverlayProgress $script:uiHash.dtUpdates = $dtUpdates $script:uiHash.tbInfo = $tbInfo $newRunspace = [runspacefactory]::CreateRunspace() $newRunspace.ApartmentState = "STA" $newRunspace.ThreadOptions = "ReuseThread" $newRunspace.Open() $newRunspace.SessionStateProxy.SetVariable("uiHash",$Script:uiHash) $newRunspace.SessionStateProxy.Path.SetLocation($script:projectRootFolder) $psCmdUpdateOutdated = [PowerShell]::Create().AddScript({ $ErrorActionPreference = "Stop" try { . ".\helpers.ps1" Write-Logs -Message "Searching for outdated packages" -LogLevel "Info" $uiHash.OutdatedPackages = @(Start-Choco -Command "outdated" -Options "--ignore-unfound") Write-Logs -Message "$($uiHash.OutdatedPackages.Count) outdated packages found" -LogLevel "Info" $script:uiHash.window.Dispatcher.Invoke([System.Action] { try { foreach ($package in $uiHash.OutdatedPackages) { $script:uiHash.dtUpdates.Rows.Add(@( $true, $package.PackageName, $package.currentVersion, $package.newVersion )) } $script:uiHash.tbInfo.Text = Get-UpdateInfo -dtUpdates $uiHash.dtUpdates } catch { Write-Logs -Message "Updating list of outdated packages failed with error '$_' on line $($_.InvocationInfo.Line)" -LogLevel "Error" } finally { $script:uiHash.currentAction = "None" # This will end the Progress overlay $script:uiHash.spOverlay.Visibility = "Collapsed" $script:uiHash.gOverlay.Visibility = "Collapsed" $script:uiHash.tbInfo.Visibility = "Visible" if ($script:uiHash.dgUpdates.Items.Count -gt 0) { $script:uiHash.dgUpdates.Visibility = "Visible" } } }, "Normal" ) } catch { Write-Logs -Message "Updating outdated packages failed with error '$_' on line $($_.InvocationInfo.Line)" -LogLevel "Error" } }) $psCmdUpdateOutdated.Runspace = $newRunspace $script:handleSearch = $psCmdUpdateOutdated.BeginInvoke() } function Install-Updates { $script:uiHash.currentAction = "Install" Show-Overlay -Text "Installing updates" $script:uiHash.window = $window $script:uiHash.gOverlay = $gOverlay $script:uiHash.spOverlay = $spOverlay $script:uiHash.tbOverlay = $tbOverlay $script:uiHash.tbOverlayProgress = $tbOverlayProgress $script:uiHash.dtUpdates = $dtUpdates $script:uiHash.tbInfo = $tbInfo $script:uiHash.currentAction = $script:currentAction $newRunspace = [runspacefactory]::CreateRunspace() $newRunspace.ApartmentState = "STA" $newRunspace.ThreadOptions = "ReuseThread" $newRunspace.Open() $newRunspace.SessionStateProxy.SetVariable("uiHash",$Script:uiHash) $newRunspace.SessionStateProxy.Path.SetLocation($script:projectRootFolder) $psCmdInstallUpdates = [PowerShell]::Create().AddScript({ $ErrorActionPreference = "Stop" try { . ".\helpers.ps1" Write-Logs -Message "Installing packages" -LogLevel "Info" $PackageList = ($uiHash.dtUpdates | Group-Object -AsHashTable -Property doUpdate).True.PackageName Write-Logs -Message "Silent state: $($uiHash.Options.Silent)" -LogLevel "Debug" Write-Logs -Message "Hidden state: $($uiHash.Options.Hidden)" -LogLevel "Debug" Write-Logs -Message "WhatIf state: $($uiHash.Options.WhatIf)" -LogLevel "Debug" $ArgumentList = @("upgrade") if ( $uiHash.Options.Silent -or $uiHash.Options.Hidden) { $ArgumentList += @("-y") } if ( $uiHash.Options.WhatIf ) { $ArgumentList += @("--noop") } $ArgumentList += $PackageList $ProcessSplat = @{ FilePath = "choco" ArgumentList = $ArgumentList -join ' ' Wait = $True PassThru = $True Verb = "RunAs" } if ( $uiHash.Options.Hidden) { $ProcessSplat.WindowStyle = "Hidden" } Start-Process @ProcessSplat Write-Logs -Message "Installing packages finished" -LogLevel "Info" $script:uiHash.window.Dispatcher.Invoke( [System.Action] { $script:uiHash.currentAction = "AfterInstall" $script:uiHash.spOverlay.Visibility = "Collapsed" $script:uiHash.gOverlay.Visibility = "Collapsed" }, "Normal" ) } catch { Write-Logs -Message "Installing chocolatey packages failed with error '$_' on line $($_.InvocationInfo.Line)" -LogLevel "Error" } }) $psCmdInstallUpdates.Runspace = $newRunspace $script:handleInstall = $psCmdInstallUpdates.BeginInvoke() } function Show-Overlay { [CmdletBinding()] param( [Parameter(Mandatory = $True)] [String]$Text ) $tbOverlay.Text = $Text if ($gOverlay.IsVisible -eq $False) { # do not start the overlay process again. instead just update the text $gOverlay.Visibility = "Visible" $spOverlay.Visibility = "Visible" $dgUpdates.Visibility = "Collapsed" $tbInfo.Visibility = "Collapsed" $progressRunspace = [runspacefactory]::CreateRunspace() $progressRunspace.ApartmentState = "STA" $progressRunspace.ThreadOptions = "ReuseThread" $progressRunspace.Open() $progressRunspace.SessionStateProxy.SetVariable("uiHash",$Script:uiHash) $progressRunspace.SessionStateProxy.Path.SetLocation($script:projectRootFolder) $psCmdProgress = [PowerShell]::Create().AddScript({ $ErrorActionPreference = "Stop" try { . ".\helpers.ps1" while ($uiHash.currentAction -ne "None") { Start-Sleep -Milliseconds 500 $script:uiHash.window.Dispatcher.Invoke([System.Action] { switch($uiHash.tbOverlayProgress.Text) { "" { $uiHash.tbOverlayProgress.Text = "." } "." { $uiHash.tbOverlayProgress.Text = ".." } ".." { $uiHash.tbOverlayProgress.Text = "..." } "..." { $uiHash.tbOverlayProgress.Text = "" } default { $uiHash.tbOverlayProgress.Text = "." } } }, "Normal" ) } } catch { Write-Logs -Message "Updating search progress failed with error '$_' on line $($_.InvocationInfo.Line)" -LogLevel "Error" } }) $psCmdProgress.Runspace = $progressRunspace $script:handleProgress = $psCmdProgress.BeginInvoke() } } function Test-ChocolateyInstall { [CmdletBinding()] [OutputType([boolean])] # Just doing a lazy check here. If it is not found in $PATH it won't work anyway $choco = Get-Command -Name choco -CommandType Application -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source if ( $null -eq $choco -or [String]::IsNullOrWhiteSpace($choco) ) { # choco not found return $false } else { # choco found return $true } } function Test-Settings { [CmdletBinding()] param ( [Parameter( Mandatory = $False )] [PSCustomObject] $settings ) begin { if ($null -eq $settings) { $settings = [PSCustomObject]@{} } } process { if ( !($settings.choco_options) ) { $ChocoOptionsTree = [PSCustomObject]@{ silent = $False hidden = $False whatIf = $False } $settings | Add-Member -NotePropertyName "choco_options" -NotePropertyValue $ChocoOptionsTree } else { if ( !($settings.choco_options.psobject.Properties.name -contains "silent") ) { $settings.choco_options | Add-Member -NotePropertyName "silent" -NotePropertyValue $False} if ( !($settings.choco_options.psobject.Properties.name -contains "hidden") ) { $settings.choco_options | Add-Member -NotePropertyName "hidden" -NotePropertyValue $False} if ( !($settings.choco_options.psobject.Properties.name -contains "whatIf") ) { $settings.choco_options | Add-Member -NotePropertyName "whatIf" -NotePropertyValue $False} } if ( !($settings.updater) ) { $updaterTree = [PSCustomObject]@{ checkVersionOnStartup = $true proxy = $null proxyUserName = $null proxyPassword = $null timeout = 5 } $settings | Add-Member -NotePropertyName "updater" -NotePropertyValue $updaterTree } else { if ( !($settings.updater.psobject.Properties.name -contains "checkVersionOnStartup") ) { $settings.updater | Add-Member -NotePropertyName "checkVersionOnStartup" -NotePropertyValue $true } if ( !($settings.updater.psobject.Properties.name -contains "proxy") ) { $settings.updater | Add-Member -NotePropertyName "proxy" -NotePropertyValue $null } if ( !($settings.updater.psobject.Properties.name -contains "proxyUserName") ) { $settings.updater | Add-Member -NotePropertyName "proxyUserName" -NotePropertyValue $null } if ( !($settings.updater.psobject.Properties.name -contains "proxyPassword") ) { $settings.updater | Add-Member -NotePropertyName "proxyPassword" -NotePropertyValue $null } if ( !($settings.updater.psobject.Properties.name -contains "timeout") ) { $settings.updater | Add-Member -NotePropertyName "timeout" -NotePropertyValue 5 } } if ( !($settings.general) ) { $generalTree = [PSCustomObject]@{ ignoreStartUpChecks = $false } $settings | Add-Member -NotePropertyName "general" -NotePropertyValue $generalTree } else { if ( !($settings.general.psobject.Properties.name -contains "ignoreStartUpChecks") ) { $settings.general | Add-Member -NotePropertyName "ignoreStartUpChecks" -NotePropertyValue $true } } $settings } } function Test-Version { $RestSplat = @{ Uri = "https://api.github.com/repos/we-mi/psChocoUpdateNotify/releases" TimeoutSec = $settings.updater.timeout } if ( -not [String]::IsNullOrWhiteSpace($settings.updater.proxy) ) { $RestSplat.Proxy = $settings.updater.proxy if ( -not [String]::IsNullOrWhiteSpace($settings.updater.proxyUserName) -and -not [String]::IsNullOrWhiteSpace($settings.updater.proxyPassword)) { $cred = New-Object System.Management.Automation.PSCredential -ArgumentList $settings.updater.proxyUserName, (ConvertTo-SecureString $settings.updater.proxyPassword -AsPlainText -Force) $RestSplat.ProxyCredential = $cred } } try { $result = Invoke-RestMethod @RestSplat if ($result) { $GitHubVersion = [version]($result.name -replace '^v') if ( [version]$script:version -lt $GitHubVersion ) { return $GitHubVersion } else { return $null } } else { return $null } } catch { return $null } } |