Recently, a mysterious bug cropped up in one of the programs for which I had most of the responsibility. In this case the bug was a strange “The Program cannot access the specified file because it is in use by another process”
One particularly helpful trait to discovering the cause was that exceptions of this nature are actually logged to the Database, so I was able to determine with some accuracy where the exception was occurring. This was in an “Updater” program; the Exception was occurring when it tried to launch the installer it just finished downloading.
To me that didn’t make a whole lot of sense. I was unable to reproduce the error very often and when I Did I got the exception when the Updater attempted to download to a temporary file that was still in use (which I worked around anyway by attempting to find a filename that wasn’t present at all).
My suspicion is that when the Update Application finishes downloading the file, a Anti-virus program is accessing it on the machines with the issue. The problem appears to only occur for the larger updates, which still seems reasonable with this theory since larger updates would take longer to scan. Also, an AV program typically takes an exclusive lock on items it is scanning while it scans.
Even so, it is still only a guess. The only way to confirm it would be to somehow figure out and keep track of what other program(s) are using the file to try to figure out what is locking it.
On Windows XP, we would be out of luck- at least for any simple solution. It would certainly be possible to add this feature but the advanced memory spelunking Would be a pain- and it would require the use of subject-to-change functions like ntQuerySystemInformation or even a Driver, which is probably going a bit far.
Thankfully, Vista, 7, and 8 have a feature called a “Restart Manager”. The Restart Manager tracks open handles, so if we create a session and try to register a resource we can determine not only if a file is locked, but what processes are using that file.
The Basic steps are reasonably simple, given the complexity of the other alternatives:
- Start a Restart Manager Session with rmStartSession
- Use rmRegisterResources to attempt to register the file in question.
- Use the rmGetList Function to retrieve a list of Processes using said file.
- End the Restart Manager Session
The approach is relatively straightforward to create a class to provide this information:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
static public class LockUtil { [StructLayout(LayoutKind.Sequential)] struct RM_UNIQUE_PROCESS { public int dwProcessId; public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime; } const int RmRebootReasonNone = 0; const int CCH_RM_MAX_APP_NAME = 255; const int CCH_RM_MAX_SVC_NAME = 63; enum RM_APP_TYPE { RmUnknownApp = 0, RmMainWindow = 1, RmOtherWindow = 2, RmService = 3, RmExplorer = 4, RmConsole = 5, RmCritical = 1000 } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct RM_PROCESS_INFO { public RM_UNIQUE_PROCESS Process; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] public string strAppName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] public string strServiceShortName; public RM_APP_TYPE ApplicationType; public uint AppStatus; public uint TSSessionId; [MarshalAs(UnmanagedType.Bool)] public bool bRestartable; } [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] static extern int RmRegisterResources(uint pSessionHandle, UInt32 nFiles, string[] rgsFilenames, UInt32 nApplications, [In] RM_UNIQUE_PROCESS[] rgApplications, UInt32 nServices, string[] rgsServiceNames); [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)] static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey); [DllImport("rstrtmgr.dll")] static extern int RmEndSession(uint pSessionHandle); [DllImport("rstrtmgr.dll")] static extern int RmGetList(uint dwSessionHandle, out uint pnProcInfoNeeded, ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[] rgAffectedApps, ref uint lpdwRebootReasons); public static List<Process> WhoIsLocking(String path) { return WhoIsLocking(new string[] { path }); } /// <summary> /// Find out what process or processes have a lock on the specified file. /// </summary> /// <param name="path">Path of the files.</param> /// <returns>List of Processes locking the files</returns> public static List<Process> WhoIsLocking(string[] paths) { uint handle; string key = Guid.NewGuid().ToString(); List<Process> processes = new List<Process>(); int res = RmStartSession(out handle, 0, key); if (res != 0) throw new Exception("Restart Manager Session could not be started."); try { const int ERROR_MORE_DATA = 234; uint pnProcInfoNeeded = 0, pnProcInfo = 0, lpdwRebootReasons = RmRebootReasonNone; string[] resources = paths; // Just checking on one resource. res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null); if (res != 0) throw new Exception("Could not register resource."); //First call to rmGetList() returns the total numberof processes, but when called //again the actual number may have increased. res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons); if (res == ERROR_MORE_DATA) { // This takes me back to FindNextFile() in a way. Except we can grab all the results // simultaneously, which is nice. RM_PROCESS_INFO[] processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded]; pnProcInfo = pnProcInfoNeeded; // Get the list res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons); if (res == 0) { processes = new List<Process>((int)pnProcInfo); //Enumerate the results... for (int i = 0; i < pnProcInfo; i++) { try { //we have a ProcessID, and the Process has a static for retrieving //a Process object by ID. //This can fail if we don't have permission to access the process as well //as if the Process ID in question has since terminated. processes.Add(Process.GetProcessById(processInfo[i].Process.dwProcessId)); } catch (ArgumentException) { } } } else throw new Exception("Failed to list Program(s) locking given resources."); } else if (res != 0) throw new Exception("Could not list processes locking resources. Failed to get size of result."); } finally { //End the session. RmEndSession(handle); } return processes; } } |
At this point I had a way to retrieve the processes locking a set of files (normally for my uses, one file). leveraging it in the Update Program wouldn’t be entirely straightforward- the intent was to have that information logged, which means it would need to somehow be a part of the Exceptions “ToString()” result. In order to do this I simply created a new Exception:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class LockedFileException : Exception { private List<Process> _OtherProcesses; private Exception _InnerException; public List<Process> LockingProcesses { get { return _OtherProcesses; } } public LockedFileException(String pFile,Exception pInnerException) { try { _OtherProcesses = LockUtil.WhoIsLocking(pFile); } catch (Exception exx) { _OtherProcesses = null; } _InnerException = pInnerException; } public override string ToString() { StringBuilder sb = new StringBuilder(); sb.Append("File has been locked by:" + String.Join(",", _OtherProcesses) + "\n"); sb.Append(InnerException.ToString()); return base.ToString(); } } |
With the new Exception in place, I was able to use it- My primary interest was in knowing what processes were accessing a file when it was in use. This Exception could be tracked specifically- it would be a Win32Exception with an HRESULT of 0x80004005:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
catch(System.ComponentModel.Win32Exception winexcept) { if(winexcept.ErrorCode==0x80004005) { //The process is in use by another process. updatedrawdata.DownloadError = new LockedFileException(installfile,winexcept); updatedrawdata.StringDraw = "Locked by:" + String.Join(",", from s in ((LockedFileException)updatedrawdata.DownloadError).LockingProcesses select s.ProcessName); ErroredItems.Add(loopdownloaded); updateItemData(lvi); InstallHistory.AddHistory(loopdownloaded, updatedrawdata.DownloadError); return false; } } |
Of course most of that is specific to the Updater implementation; essentially it will track when an updating item encounters an exception and use that to change it’s ListItem’s ProgressBar to be Red, as well as display a cursory set of exception information. The Information is also tracked in the PostGres database in the history, so it can be referred to later.
If the new version works I should be able to determine what other processes were locking the file simply be using the Installation History feature of the Updater. This can help me prove or disprove the possibility of it being Anti-virus related.
Have something to say about this post? Comment!