With this code, I am opening excel(with visible = false so the user cannot see it), writing to a workbook, and then either opening excel(making it visible) after the script ends or closing it completely without saving. When I save excel, leave it open, end the script, and then manually close excel later, there are no background processes in the task manager. However, when I close excel with the script, it remains in the task manager.
Here is how I start excel:
$script:excel = new-object -ComObject excel.application # create excel object
$excel.visible = $false # hide excel window
$script:workbook = $excel.Workbooks.Add() # add excel file
$script:ws1 = $workbook.Worksheets.Item(1) # create new sheet
Here is how I close it:
[gc]::Collect()
[gc]::WaitForPendingFinalizers()
if ($script:closeOnX) {
#only do this if not keeping excel open
Write-Host "Closing Excel"
$excel.Quit()
}
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel)
closeOnX is just a flag so it only actually closes the excel app on certain occasions. The rest is executed each time the script ends.
When I end the script and close excel at the same time, I want only the current excel process to close (which is why I don't want to stop-process) and not close other workbooks that the user may be working on.
When I end the script, save and open the excel, I want all the processes to be gone when the user manually closes excel. (This is working)
tl;dr
Use the following idiom to ensure that all references to COM objects are released, which, in combination with calling $excel.Quit()
, ensures that the Excel process (eventually) terminates:
& { # Create a temporary child scope.
$excel = New-Object -ComObject excel.application # create excel object
# ... work with the $excel object and its object model,
# using whatever local variables needed.
# You must *always* call .Quit(), otherwise the Excel process lingers
# for the entire OS user session.
$excel.Quit()
} # All variables created inside { ... } go out of scope
# when this block is exited, ensuring release of all COM objects.
For general guidance on how to release (Excel) COM objects, see the bottom section.
$excel.Quit()
is enough to eventually terminate the Excel process, but when that happens depends on when the garbage collector happens to run the next time.
Your attempt to explicitly release Excel with [System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel)
is insufficient, because variables $script:workbook
and $script:ws1
still have references to Excel COM objects that will not be released until the variables have gone out of scope and these references are eventually garbage collected.
Therefore, in order to speed up the release, you must release these references explicitly too, before running the garbage collector:
$script:excel = new-object -ComObject excel.application # create excel object
$script:workbook = $excel.Workbooks.Add() # add a workbook
$script:ws1 = $workbook.Worksheets.Item(1) # reference the 1st sheet
# ...
# You must *always* call .Quit(), otherwise the Excel process lingers
# for the entire OS user session.
$script.excel.Quit()
# Relinquish references to *all* Excel objects.
$script:excel = $script:workbook = $script:ws1 = $null
# Alternative:
# Remove-Variable -Scope Script excel, workbook, ws1
# With all references released, running the garbage collector
# should now release the COM objects and terminate Excel
# shortly after.
[GC]::Collect()
# Note that calling [GC]::WaitForPendingFinalizers() afterwards
# to wait for *completion* of the *doesn't work here*,
# because the CLR-managed RCWs (Runtime-Callable Wrappers for COM objects)
# do not guarantee deterministic release of the underlying COM objects.
Preferable alternative:
& { ... }
:# PREFERABLE ALTERNATIVE to the code above:
& { # Create a temporary child scope.
$excel = new-object -ComObject excel.application # create excel object
$workbook = $excel.Workbooks.Add() # add a workbook
$ws1 = $workbook.Worksheets.Item(1) # reference the 1st sheet
# You must *always* call .Quit(), otherwise the Excel process lingers
# for the entire OS users session.
$excel.Quit()
} # On exiting this block, $excel, $workbook, and $ws1
# go out of scope and release the COM objects when the
# garbage collector runs next.
# Run the garbage collector now.
# The Excel process should terminate shortly after.
[GC]::Collect()
ALWAYS call .Quit()
- without it, the Excel process that is created behind the scenes is never terminated, not even when the PowerShell session ends (of course, it is terminated when the OS user session as a whole ends).
$excel.Quit()
is usually all that is needed (unless global variables variables are used to store references to Excel objects), because with the script / function variables that reference COM objects going out of scope, the underlying COM objects are eventually released automatically too.
If you want the COM objects to be released as quickly as possible:
You must release references to all COM objects you've stored in individual variables:
[System.Runtime.InteropServices.Marshal]::ReleaseComObject()
calls; because there is a simpler and more robust alternative:$null
.Remove-Variable
& { ... }
block, which means that the references will implicitly be released on leaving the child scope.These approaches are not only simpler and more concise than calling [System.Runtime.InteropServices.Marshal]::ReleaseComObject()
, but also prevent later attempts at accessing already-released COM objects.
Afterwards, call [GC]::Collect()
to force instant garbage collection - but note that your code is blocked while the garbage collector runs (albeit usually only briefly) .
If you additionally want to make sure that releasing the COM objects has completed before you continue:
Note: There's probably rarely a need for this, because Excel usually releases resources when its .Quit()
method is called, such as closing files it has open.
You can call [GC]::WaitForPendingFinalizers()
after calling [GC]::Collect()
, but it is likely not to work: The RCWs (runtime-callable wrappers) that manage access to the COM objects themselves being finalized does not guarantee release of the COM resources at that time; from the docs (emphasis added):
"When the reference count on the COM object becomes 0, the COM object is usually freed, although this depends on the COM object's implementation and is beyond the control of the runtime."
Indeed, in the case at hand the Excel process does not terminate before the [GC]::WaitForPendingFinalizers()
call returns; that only happens within a second or so afterwards.