After years of pain, I finally found a clean-and-definitive way to get rid of the dreadful issue Could not copy assembly, the process cannot access the file because it is used by another process!
I have no idea how many .NET developers are coping with this issue, but on our side it used to be daily and there are many situations where an assembly file gets locked:
- sometimes VS just load in-process the assembly, or the PDB file or the .xml documentation file, for an unknown reason (usually after a debug session)
- sometimes the culprit seems to be vshost.exe
- sometimes the culprit is the test runner process
- sometimes, after a smoke test session, one just forgets to close all processes that hold assemblies…
- …or sometimes it is just a zombie process that should have stopped but just didn’t!
- and when developing a VS extension, all of those situations are happening more often
When the culprit process was not obviously identified, I started Process Explorer to kill it! And when the culprit process is my current VS instance, it means I need to restart it and interrupt current work for significant time and then lose my mental focus!!
The clean-and-definitive solution to this problem, is this script to copy in a .bat file. This script must be invoked from the VS project pre-build-events. It just moves the assembly file, the .pdb file and the .xml documentation file to a temporary location (if the .pdb or .xml file is missing no problem).
The key is that when a process locks a file, Windows authorizes to move and rename the file. I found the original idea from this stackoverflow answer, that itself found it from a Keyvan Nayyeri blog post that seems to have been removed.
REM This script is invoked before compiling an assembly, and if the target file exist, it moves it to a temporary location
REM The file-move works even if the existing assembly file is currently locked-by/in-use-in any process.
REM This way we can be sure that the compilation won't end up claiming the assembly cannot be erased!
echo $(TargetPath) is %1
echo $(TargetFileName) is %2
echo $(TargetDir) is %3
echo $(TargetName) is %4
if not exist %dir% (mkdir %dir%)
REM delete all assemblies moved not really locked by a process
del "%dir%\*" /q
REM assembly file (.exe / .dll) - .pdb file and eventually .xml file (documentation) are concerned
REM use %random% to let coexists several processes that hold several versions of locked assemblies
if exist "%1" move "%1" "%dir%\%2.locked.%random%"
if exist "%3%4.pdb" move "%3%4.pdb" "%dir%\%4.pdb.locked%random%"
if exist "%3%4.xml.locked" del "%dir%\%4.xml.locked%random%"
REM Code with Macros
REM if exist "$(TargetPath)" move "$(TargetPath)" "C:\temp\LockedAssemblies\$(TargetFileName).locked.%random%"
REM if exist "$(TargetDir)$(TargetName).pdb" move "C:\temp\LockedAssemblies\$(TargetName).pdb" "$(TargetDir)$(TargetName).pdb.locked%random%"
REM if exist "$(TargetDir)$(TargetName).xml.locked" del "C:\temp\LockedAssemblies\$(TargetName).xml.locked%random%"
REM PreBuildEvent code
REM $(SolutionDir)\BuildProcess\PreBuildEvents.bat "$(TargetPath)" "$(TargetFileName)" "$(TargetDir)" "$(TargetName)"
To invoke this script just add this in all your VS project pre-build events command line. If you wish the script name and location to be different, just adapt
$(SolutionDir)\BuildProcess\PreBuildEvents.bat "$(TargetPath)" "$(TargetFileName)" "$(TargetDir)" "$(TargetName)"
Notice that this script does the job no matter the actual configuration (Debug or Release) and no matter if the compilation is started from VS or from any other script.