Start-WindowsCleanup

Discussion in 'Scripting' started by GodHand, Sep 18, 2019.

  1. Windows_Addict

    Windows_Addict MDL Addicted

    Jul 19, 2018
    650
    1,232
    30
    @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.
     
  2. abbodi1406

    abbodi1406 MDL KB0000001

    Feb 19, 2011
    11,225
    48,300
    340
    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
        )
    
    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;}"
    
     
  3. BetADNA

    BetADNA MDL Junior Member

    Jul 26, 2015
    90
    57
    0
    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?!?!
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  4. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    529
    834
    30
    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.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  5. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    529
    834
    30
    Updated 06-16-2020
    - Improved the removal helper function.
    - Added the removal of the icon and thumbnail cache databases.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  6. BetADNA

    BetADNA MDL Junior Member

    Jul 26, 2015
    90
    57
    0
    Thanks for the Update :)
    Tested it on multiple PC´s and it works very well, better then ccleaner and all those s**tty tools :p

    Boot times are normal again now.. dunno what it was...

    Greetz
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  7. adric

    adric MDL Addicted

    Jul 30, 2009
    754
    397
    30
    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;}"
     
  8. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    529
    834
    30
    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.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  9. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    529
    834
    30
    (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.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  10. mbk1969

    mbk1969 MDL Novice

    Jun 16, 2020
    4
    9
    0
    #50 mbk1969, Jun 17, 2020
    Last edited: Jun 18, 2020
    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
    
     
  11. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    529
    834
    30
    #51 GodHand, Jun 17, 2020
    Last edited: Jun 17, 2020
    (OP)
    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.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  12. mbk1969

    mbk1969 MDL Novice

    Jun 16, 2020
    4
    9
    0
    #52 mbk1969, Jun 17, 2020
    Last edited: Jun 17, 2020
    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.
     
  13. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    529
    834
    30
    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.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  14. mbk1969

    mbk1969 MDL Novice

    Jun 16, 2020
    4
    9
    0
    #54 mbk1969, Jun 17, 2020
    Last edited: Jun 18, 2020
    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
    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.
     
  15. drew84

    drew84 MDL Addicted

    Mar 13, 2014
    734
    1,197
    30
    ... 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
     
  16. mbk1969

    mbk1969 MDL Novice

    Jun 16, 2020
    4
    9
    0
    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.
     
  17. GodHand

    GodHand MDL Addicted

    Jul 15, 2016
    529
    834
    30
    #57 GodHand, Jun 21, 2020
    Last edited: Jun 21, 2020
    (OP)
    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.
     
    Stop hovering to collapse... Click to collapse... Hover to expand... Click to expand...
  18. verndog

    verndog MDL Junior Member

    May 3, 2010
    78
    12
    0
    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
     
  19. abbodi1406

    abbodi1406 MDL KB0000001

    Feb 19, 2011
    11,225
    48,300
    340
    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';}"
    
     
  20. verndog

    verndog MDL Junior Member

    May 3, 2010
    78
    12
    0
    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.