@Nikos I tried a few things but couldn't manage to pass multi parameters with function. Although it worked fine with single parameter e.g. Code: -Include 'RestorePoints', 'EventLogs', 'DuplicateDrivers'; but not with two and more, Code: -Include 'RestorePoints', 'EventLogs', 'DuplicateDrivers' -ComponentCleanup; Maybe @abbodi1406 can help.
It seems an issue in Start-WindowsCleanup itself, it does not accept more than one named parameter at a time maybe @GodHand can shed some light on this i removed ParameterSetName from parameters and it worked in my test i.e. Code: Param ( [Parameter(HelpMessage = 'Includes the clean-up of Downloads, Restore Points, Event Logs, Google Chrome, Mozilla Firefox, Internet Explorer and Microsoft Edge.')] [ValidateSet('Downloads', 'RestorePoints', 'EventLogs', 'DuplicateDrivers', 'Chrome', 'Firefox', 'IE', 'Edge')] [String[]]$Include = $null, [Parameter(HelpMessage = 'Outputs a Gridview GUI list of all of the values in the -Include parameter allowing for the selection of items to include in the removal process as opposed to manually entering them.')] [Switch]$GUI, [Parameter(HelpMessage = 'Removes any user-specific file, folder or directory passed to the parameter when the function is called. This can be a single object or an array of multiple objects.')] [String[]]$Additional, [Parameter(HelpMessage = 'Removes all superseded components in the component store.')] [Switch]$ComponentCleanup, [Parameter(HelpMessage = 'Removes all superseded components in the component store and also resets the image base, further reducing the size of the component store.')] [Switch]$ResetBase, [Parameter(HelpMessage = 'Can only be used with the Additional and ResetBase parameters.')] [Switch]$Force ) Spoiler Start_Start-WindowsCleanup.cmd Code: @echo off reg query HKU\S-1-5-19 1>nul 2>nul || ( echo ==== Error ==== echo Right click on this file and select 'Run as administrator' echo Press any key to exit... pause >nul exit /b ) set "_work=%~dp0" setlocal EnableDelayedExpansion pushd "!_work!" REM File name Function name powershell -nop -ep bypass -c "& {. .\Start-WindowsCleanup.ps1; Start-WindowsCleanup -Include EventLogs,IE,Edge -ComponentCleanup -ResetBase -Force;}"
works perfect for me.. Just a question, is it normal that after cleaning windows with this, that the boot time increases? my boot times are much longer now?!?!
Updated 06-15-2020 - Corrected an issue where some parameters would not process. - Removed some unnecessary code. - Added the removal of recent document history. - Improved verbosity.
Updated 06-16-2020 - Improved the removal helper function. - Added the removal of the icon and thumbnail cache databases.
Thanks for the Update Tested it on multiple PC´s and it works very well, better then ccleaner and all those s**tty tools Boot times are normal again now.. dunno what it was... Greetz
new version still ignores all - parameters after the first two are processed. I.e. -ResetBase is not executing Code: powershell -nop -ep bypass -c "& {. .\Start-WindowsCleanup.ps1; Start-WindowsCleanup -Include EventLogs,IE,Edge -ComponentCleanup -ResetBase -Force;}"
From the information header of the function: -ComponentCleanup and -ResetBase do two different things and only one or the other can be used at once per session. If both are enabled then the function disables the image base reset and just performs a component store clean-up. Considering you're using the -Force switch - which is only applicable to resetting the image base on builds 18362+ - you only need to pass the -ResetBase switch, not the -ComponentCleanup switch.
(2nd update for 06-16-2020) - Since some people prefer not to have cache databases reset each time they run this function, the resetting and removal of the icon and thumbnail databases no longer occurs automatically and they have instead been added to the -Includes parameter and -GUI switch. - The order in which certain removals are processed have been restructured.
In the code of the function "Stop-Running" you use PowerShell cmdlets to organize a cycles for closing of the services and of the processes. I would use .Net objects System.Diagnostics.Process and System.ServiceProcess.ServiceController to do that: Code: If ($PSItem -is [Diagnostics.Process]) { If ($PSItem.Name -eq 'explorer') { Try { $PSItem.Kill() } Catch { } } Else { Try { if($true -eq $PSItem.CloseMainWindow()) // send request to close the main window { $PSItem.WaitForExit(10000) // process has main window so we will wait for the exit for 10 seconds } if( !$PSItem.HasExited ) { $PSItem.Kill() } } Catch { } } } ElseIf ($PSItem -is [ServiceProcess.ServiceController]) { If ($PSItem.Status -ne 'Stopped') { Try { if( $PSItem.Status -ne 'StopPending' ) { $PSItem.Stop() // stop the service } $PSItem.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(10)) // wait for service to stop for 10 seconds } Catch { } } } Your code for processes seems flawed because first it collects the processes by name, and then it closes them getting the process by name again - that second request for the process by name can return not the same process. In my code example the processes already collected by name get closed by the instance itself. Upd: Scratch that ^, now I understand the logic behind. *** You do not need Where-Object there: Code: $Running = Get-Process | Where-Object -Property Name -Like *$Object* If (!$Running) { $Running = Get-Service | Where-Object -Property Name -EQ $Object } because both Get-Process and Get-Service cmdlets can accept name with wildcards: Code: $Running = Get-Process -Name *$Object* If (!$Running) { $Running = Get-Service -Name $Object } *** No need to execute "Get-WmiObject -Class Win32_ShadowCopy" twice: Code: 'RestorePoints' { # Delete all system shadow copies if the -Include parameter with the 'RestorePoints' value is used. If (Get-WmiObject -Class Win32_ShadowCopy) { Get-WmiObject -Class Win32_ShadowCopy | ForEach-Object -Process { Write-Verbose ('Performing the operation "Delete ShadowCopy" on target "{0}"' -f $PSItem.ID) -Verbose $PSItem.Delete() } } } => Code: 'RestorePoints' { # Delete all system shadow copies if the -Include parameter with the 'RestorePoints' value is used. Foreach($sc in (Get-WmiObject -Class Win32_ShadowCopy)) { Write-Verbose ('Performing the operation "Delete ShadowCopy" on target "{0}"' -f $sc.ID) -Verbose $sc.Delete() } } *** Why do you use PowerShell facility to run DISM and not the same System.Diagnostics.Process as with cleanmgr.exe? I see that you check the output of the DISM but what for? And, BTW, you can use cmdlet Start-Process with the same result as your [Diagnostics.Process] code: Code: # Start the Microsoft Windows Disk Clean-up utility in advanced mode as a .NET process. $ProcessInfo = New-Object -TypeName Diagnostics.ProcessStartInfo $ProcessInfo.FileName = '{0}' -f "$Env:SystemRoot\System32\cleanmgr.exe" $ProcessInfo.Arguments = '/SAGERUN:{0}' -f $StateFlags $ProcessInfo.CreateNoWindow = $true $Process = New-Object -TypeName Diagnostics.Process $Process.StartInfo = $ProcessInfo Write-Verbose "Running the Windows Disk Clean-up utility in advanced mode as a .NET process." -Verbose [Void]$Process.Start() $Process.WaitForExit() If ($null -ne $Process) { $Process.Dispose() } => Code: $Process = Start-Process -FilePath "$Env:SystemRoot\System32\cleanmgr.exe" -ArgumentList ('/SAGERUN:{0}' -f $StateFlags) -WindowStyle Hidden -PassThru If ($null -ne $Process) { $Process.WaitForExit() $Process.Dispose() } Upd: or even shorter (since the script does not need the instance of the process) Code: Start-Process -FilePath "$Env:SystemRoot\System32\cleanmgr.exe" -ArgumentList "/SAGERUN:{$StateFlags}" -WindowStyle Hidden -Wait
These are all I had to read to ascertain that you're watering down code you do not understand and are unfamiliar with how comparative operators work (nor how command verbosity works). *$Object* is used for Get-Process because there can be multiple processes running with the same name in addition to an integer value, or contain additional values. An example is Microsoft Edge HTML. Conversely, when closing a service you do not want to use wildcards if the purpose of the code is to only close the service with the name identical to the one that's passed. If you had worked with processes and/or services with PowerShell before, and further looked at what the function did within the main function itself, you'd know that. If you knew how monitored PowerShell jobs work, you'd understand why DISM is not run using the Diagnostic.Process .NET class - it's because a PowerShell job runs in the background allowing for commands (and in the case of this function - loops) to be run simultaneously. A DISM job is used in order to continually loop through the content DISM log and return the percentage in 5 second intervals. Invoke-Command can also do this with its -AsJob switch. This is done for 2 reasons: 1) It allows you to dictate how details from a running job are returned; 2) Returns that information to the console while the job is running as opposed to after it has completed. Likewise, one could add a search for a specific error (or any kind of string) while the script job is running and perform additional tasks if said string is returned. A very basic example: Code: $DISMError = Get-Content -Path $DISMLog -Raw | Select-String -Pattern Error If ($null -ne $DISMError) { Write-Warning ('The following error occurred: {0}' -f $DISMError) } Since the above error is being returned at the same time the job is running, one could further decide to terminate the job, compile the errors for outputting, ignore the error and let the job continue, or issue another command pertaining to said error. This is a very, very small example, though considering these comments, any advanced examples I could write would likely be beyond your understanding of how PowerShell works.
That`s not what I meant. The code $Running = Get-Process | Where-Object -Property Name -Like *$Object* gives the same results as $Running = Get-Process -Name *$Object* - you just do not need to pipeline the results of Get-Process to Where-Object to filter out the needed processes because you can pass the name with wildcards directly to the Get-Process. And the code $Running = Get-Service | Where-Object -Property Name -EQ $Object gives the same results as $Running = Get-Service -Name $Object - you just do not need to pipeline the results of Get-Service to Where-Object to filter out the needed services because you can pass the name directly to the Get-Service. You can check exit code with Diagnostic.Process, in case of errors it will be non-zero. And when you start the process both with Start-Process and with .Net code like in your script the process is executed on the background letting the script to execute further. And with Start-Process you can redirect the output (standard and error) to specified text file. Code: If ($ComponentCleanup.IsPresent -or $ResetBase.IsPresent) { $DismArgs = "/Online /Cleanup-Image /StartComponentCleanup" If ($ResetBase.IsPresent) { # Start a PowerShell Dism job to clean-up the Component Store and reset the image base. Write-Verbose "Removing all superseded components in the component store and resetting the image base." -Verbose $DismArgs = "/Online /Cleanup-Image /StartComponentCleanup /ResetBase" } Else { # Start a PowerShell Dism job to clean-up the Component Store. Write-Verbose "Removing all superseded components in the component store." -Verbose } $DISMJob = Start-Process dism.exe -ArgumentList $DismArgs -RedirectStandardOutput $DISMLog -PassThru -WindowStyle Hidden -ErrorAction SilentlyContinue if($DISMJob) { Do { Get-Content -Path $DISMLog -Tail 3 | Select-String -Pattern '%' | Select-Object -Last 1 } While ($DISMJob.WaitForExit(5000) -eq $false) $DISMLog | Remove-Items } } PS And stop assuming my level of PowerShell knowledge, be polite. I did not say that your code is bad and not working, I just noted a tweaks for a bit shorter and a bit cleaner (IMO) and a bit faster source code.
On the outset I am going to simply say I am not going to read further posts from you after your initial post because your credibility is $null (I'm sure you know what null is). I have been a part of many large PowerShell repositories (public and private) and communities for many years and no one who is in the know makes wildly obtuse posts such as your initial one. The minute I saw your first 'modified' code included object invoking methods I knew you had no idea how the function works. One of its major features is its verbosity when commands are issued. You cannot do that with invoking methods on objects. Then we have the fact you do not know what comparative operators do; instead you saw I used -Like in one and -EQ in the other and thought "oh, that's wrong!", when in fact it's specifically like that for an important purpose (which I went over). In the future I suggest you read a function in its entirety, remembering that HELPER functions are designed to accommodate the PRIMARY function, as well as any and all header data included. Conclusively, if you do have a critique or would like to offer constructive input, my recommendation is to not make it as kneejerk and pretentious as yours was. Good day.
OK, you can ignore that. But I said not a single word neither about your comparative operators nor about them not working. Obviously you can`t understand the meaning of my notes about Get-Process and Get-Service in the helper function. Start several instances of notepad and then execute Get-Process | Where-Object -Property Name -Like *notepad* and Get-Process -Name *notepad* - do you see any difference in the output? I do not because these two commands return the same results. But the second command (1) does not return all processes in the system, (2) does not use pipeline to filter notepad processes with the help of "-LIKE" operator. Second command just returns the processes with specified wildcard mask - it is shorter and it is faster. Comparison of performance Spoiler PS C:\> Measure-Command { for($i=0; $i -lt 1000; $i++) { Get-Process | Where-Object -Property Name -Like *notepad* } } Days : 0 Hours : 0 Minutes : 0 Seconds : 3 Milliseconds : 292 Ticks : 32928580 TotalDays : 3,81117824074074E-05 TotalHours : 0,000914682777777778 TotalMinutes : 0,0548809666666667 TotalSeconds : 3,292858 TotalMilliseconds : 3292,858 PS C:\> Measure-Command { for($i=0; $i -lt 1000; $i++) { Get-Process -Name *notepad* } } Days : 0 Hours : 0 Minutes : 0 Seconds : 1 Milliseconds : 197 Ticks : 11978583 TotalDays : 1,38641006944444E-05 TotalHours : 0,000332738416666667 TotalMinutes : 0,019964305 TotalSeconds : 1,1978583 TotalMilliseconds : 1197,8583 Point to the places (words, phrases) of my posts which are kneejerk and pretentious. Your wording looks to me pretentious. PS To be clear I find your script interesting and cool, I just did not understand some places (details, motivation) and wrote how I would implement them. If I was right about them then script could gain a bit, if I was wrong about them then you could point me where exactly so I could gain a bit. I should write that "introduction" in my first post before jumping to code snippets, because you interpreted my post as an attack or sneer obviously. My bad.
... First positive statement/compliment ... everyone thinks and works differently ... Hindsight .. at this point I have to admit that GodHand wasn't the only one, am really glad to see recognition that a misunderstanding may have occured.. .... and a belated welcome to MDL
Thank you. But there were no negative comments/statements in my posts. In my first post I stated that I would implement several places differently, and asked why he used particular technique starting the DISM. <dull beggar mode on> GodHand replied about DISM but his answer was a bit strange because PS jobs executed on the background is a benefit when you run pure PowerShell or .Net code in the job, but you don`t need job to start new process on the background. </dull beggar mode off> I was curious to hear the motivation behind several places in source code. Some of them I understand now, but some I still do not. No big deal. It was pure academic interest.
Updated 06-21-2020 - Start-WindowsCleanup now uses the latest strict mode. - Improved the Remove-Items helper function. - The -Force switch has been removed. Any additional items added for clean-up will automatically have their access control permissions bypassed if their initial removal attempt returns an 'Access Denied' error. - The -ComponentCleanup and -ResetBase switches have been combined into a -ComponentStore parameter which accepts either a value of 'Cleanup' or 'ResetBase'. - To perform a component store clean-up, the command -ComponentStore Cleanup can be issued. - To perform a component store clean-up with image base reset, the command -ComponentStore ResetBase can be issued. - If the -ComponentStore ResetBase parameter is issued, and the Windows build is greater than or equal to 18362, a messagebox will display requiring approval to perform a reset of the image base. - Added RetailDemo content to the default removal list.
Ever since update on 06-21-2020 abbodi's script doesnt run as before. Not sure what I'm doing wrong. Here's output: Code: ===run as admin=== cmdlet Start-WindowsCleanup at command pipeline position 1 Supply values for the following parameters: (Type !? for Help.) Include[0]: Start-WindowsCleanup.ps1 Include[1]: Start-WindowsCleanup : Cannot validate argument on parameter 'Include'. The argument "Start-WindowsCleanup.ps1" does not belong to the set "Downloads,RestorePoints,EventLogs,DuplicateDrivers,IconCache,ThumbnailCache,Chrome,Firefox,IE,Edge" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again. At line:1 char:53 + ... \Users\User\Documents\Start-WindowsCleanup.ps1; Start-WindowsCleanup; + ~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidData: (:) [Start-WindowsCleanup], ParameterBindingValidationException + FullyQualifiedErrorId : ParameterArgumentValidationError,Start-WindowsCleanup
did you changed the parameters to reflect new ones? e.g. Code: powershell -nop -ep bypass -c "& {. .\Start-WindowsCleanup.ps1; Start-WindowsCleanup -Include EventLogs,IconCache,ThumbnailCache,IE,Edge -ComponentStore ResetBase -Additional '%LocalAppData%\Microsoft\Windows\WER\ReportQueue', '%LocalAppData%\Microsoft\Windows\WindowsUpdate.log';}"
I didn't change anything. Is the above inserted into your script from Post#16? Its obvious I don't know the parameters of Windows scripting.