我公司最近升級程序經常報出更新失敗問題,究其原因,原來是更新時,他們可能又打開了正在被更新的文件,導致更新文件時,文件被其它進程占用,無法正常更新而報錯,為了解決這個問題,我花了一周時間查詢多方資料及研究,終于找到了一個查詢進程的利器:handle.exe,下載地址:https://technet.microsoft.com/en-us/sysinternals/bb896655.aspx,我是通過它來找到被占用的進程,然后KILL掉占用進程,最后再來更新,這樣就完美的解決了更新時文件被占用報錯的問題了,實現方法很簡單,我下面都有列出主要的方法,一些注意事項我也都有說明,大家一看就明白了,當然如果大家有更好的方案,歡迎交流,謝謝!
IsFileUsing:判斷文件是否被占用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | [DllImport( "kernel32.dll" )] public ?static ?extern ?IntPtr _lopen( string ?lpPathName,? int ?iReadWrite); [DllImport( "kernel32.dll" )] public ?static ?extern ?bool ?CloseHandle(IntPtr hObject); public ?const ?int ?OF_READWRITE = 2; public ?const ?int ?OF_SHARE_DENY_NONE = 0x40; public ?readonly ?IntPtr HFILE_ERROR =? new ?IntPtr(-1); private ?bool ?<strong>IsFileUsing</strong>( string ?filePath) { ???? if ?(!File.Exists(filePath)) ???? { ???????? return ?false ; ???? } ???? IntPtr vHandle = _lopen(filePath, OF_READWRITE | OF_SHARE_DENY_NONE); ???? if ?(vHandle == HFILE_ERROR) ???? { ???????? return ?true ; ???? } ???? CloseHandle(vHandle); ???? return ?false ; } |
GetRunProcessInfos:獲取指定文件或目錄中存在的(關聯的)運行進程信息,以便后面可以解除占用
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 | /// <summary> /// 獲取指定文件或目錄中存在的(關聯的)運行進程信息,以便后面可以解除占用 /// </summary> /// <param name="filePath"></param> /// <returns></returns> private ?Dictionary< int ,? string > GetRunProcessInfos( string ?filePath) { ???? Dictionary< int ,? string > runProcInfos =? new ?Dictionary< int ,? string >(); ???? string ?fileName = Path.GetFileName(filePath); ???? var ?fileRunProcs = Process.GetProcessesByName(fileName); ???? if ?(fileRunProcs !=? null ?&& fileRunProcs.Count() > 0) ???? { ???????? runProcInfos = fileRunProcs.ToDictionary(p => p.Id, p => p.ProcessName); ???????? return ?runProcInfos; ???? } ???? string ?fileDirName = Path.GetDirectoryName(filePath);? //查詢指定路徑下的運行的進程 ???? Process startProcess =? new ?Process(); ???? startProcess.StartInfo.FileName = RelaseAndGetHandleExePath(); ???? startProcess.StartInfo.Arguments =? string .Format( "\"{0}\"" , fileDirName); ???? startProcess.StartInfo.UseShellExecute =? false ; ???? startProcess.StartInfo.RedirectStandardInput =? false ; ???? startProcess.StartInfo.RedirectStandardOutput =? true ; ???? startProcess.StartInfo.CreateNoWindow =? true ; ???? startProcess.StartInfo.StandardOutputEncoding = ASCIIEncoding.UTF8; ???? startProcess.OutputDataReceived += (sender, e) => ???? { ???????? if ?(! string .IsNullOrEmpty(e.Data) && e.Data.IndexOf( "pid:" , StringComparison.OrdinalIgnoreCase) > 0) ???????? { ???????????? //var regex = new System.Text.RegularExpressions.Regex(@"(^[\w\.\?\u4E00-\u9FA5]+)\s+pid:\s*(\d+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); ???????????? var ?regex =? new ?System.Text.RegularExpressions.Regex( @"(^.+(?=pid:))\bpid:\s+(\d+)\s+" , System.Text.RegularExpressions.RegexOptions.IgnoreCase); ???????????? if ?(regex.IsMatch(e.Data)) ???????????? { ???????????????? var ?mathedResult = regex.Match(e.Data); ???????????????? int ?procId =? int .Parse(mathedResult.Groups[2].Value); ???????????????? string ?procFileName = mathedResult.Groups[1].Value.Trim(); ???????????????? if ?( "explorer.exe" .Equals(procFileName, StringComparison.OrdinalIgnoreCase)) ???????????????? { ???????????????????? return ; ???????????????? } ???????????????? //var regex2 = new System.Text.RegularExpressions.Regex(string.Format(@"\b{0}.*$", fileDirName.Replace(@"\", @"\\").Replace("?",@"\?")), System.Text.RegularExpressions.RegexOptions.IgnoreCase); ???????????????? var ?regex2 =? new ?System.Text.RegularExpressions.Regex( @"\b\w{1}:.+$" , System.Text.RegularExpressions.RegexOptions.IgnoreCase); ???????????????? string ?procFilePath = (regex2.Match(e.Data).Value ??? "" ).Trim(); ???????????????? if ?(filePath.Equals(procFilePath, StringComparison.OrdinalIgnoreCase) || filePath.Equals(PathJoin(procFilePath, procFileName), StringComparison.OrdinalIgnoreCase)) ???????????????? { ???????????????????? runProcInfos[procId] = procFileName; ???????????????? } ???????????????? else ?//如果亂碼,則進行特殊的比對 ???????????????? { ???????????????????? if ?(procFilePath.Contains( "?" ) || procFileName.Contains( "?" ))? //?亂碼比對邏輯 ???????????????????? { ???????????????????????? var ?regex3 =? new ?System.Text.RegularExpressions.Regex(procFilePath.Replace( @"\" ,? @"\\" ).Replace(". ", @" \. ").Replace(" ? ", " .{1}"), System.Text.RegularExpressions.RegexOptions.IgnoreCase); ???????????????????????? if ?(regex3.IsMatch(filePath)) ???????????????????????? { ???????????????????????????? runProcInfos[procId] = procFileName; ???????????????????????? } ???????????????????????? else ???????????????????????? { ???????????????????????????? string ?tempProcFilePath = PathJoin(procFilePath, procFileName); ???????????????????????????? regex3 =? new ?System.Text.RegularExpressions.Regex(tempProcFilePath.Replace( @"\" ,? @"\\" ).Replace(". ", @" \. ").Replace(" ? ", " .{1}"), System.Text.RegularExpressions.RegexOptions.IgnoreCase); ???????????????????????????? if ?(regex3.IsMatch(filePath)) ???????????????????????????? { ???????????????????????????????? runProcInfos[procId] = procFileName; ???????????????????????????? } ???????????????????????? } ???????????????????? } ???????????????????? else ?if ?(procFilePath.Length == filePath.Length || PathJoin(procFilePath, procFileName).Length == filePath.Length)? //其它亂碼比對邏輯,僅比對長度,如果相同交由用戶判斷 ???????????????????? { ???????????????????????? if ?(MessageBox.Show( string .Format( "發現文件:{0}可能被一個進程({1})占用,\n您是否需要強制終止該進程?" , filePath, procFileName),? "發現疑似被占用進程" , MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes) ???????????????????????? { ???????????????????????????? runProcInfos[procId] = procFileName; ???????????????????????? } ???????????????????? } ???????????????? } ???????????? } ???????? } ???? }; ???? startProcess.Start(); ???? startProcess.BeginOutputReadLine(); ???? startProcess.WaitForExit(); ???? return ?runProcInfos; } |
上述代碼邏輯簡要說明:創建一個建程來啟動handle.exe(以資源形式內嵌到項目中),然后異步接收返回數據,并通過正則表達式來匹配獲取進程數據,由于handle.exe對于中文路徑或文件名兼容不好,返回的數據存在?或其它亂碼字符,故我作了一些特殊的模糊匹配邏輯;
RelaseAndGetHandleExePath:從項目中釋放handle.exe并保存到系統的APPData目錄下,以便后續直接可以使用(注意:由于handle.exe需要授權同意后才能正常的使用該工具,故我在第一次生成handle.exe時,會直接運行進程,讓用戶選擇Agree后再去進行后面的邏輯處理,這樣雖能解決問題,但有點不太友好,目前一個是中文亂碼、一個是必需同意才能使用handle.exe我認為如果微軟解決了可能會更好)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private ?string ?RelaseAndGetHandleExePath() { ???? var ?handleInfo =? new ?FileInfo(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) +? "\\SysUpdate\\handle.exe" ); ???? if ?(!File.Exists(handleInfo.FullName)) ???? { ???????? if ?(!Directory.Exists(handleInfo.DirectoryName)) ???????? { ???????????? Directory.CreateDirectory(handleInfo.DirectoryName); ???????? } ???????? byte [] handleExeData = Properties.Resources.handle; ???????? File.WriteAllBytes(handleInfo.FullName, handleExeData); ???????? var ?handleProc = Process.Start(handleInfo.FullName); //若第一次,則彈出提示框,需要點擊agree同意才行 ???????? handleProc.WaitForExit(); ???? } ???? return ?handleInfo.FullName; } |
PathJoin:拼接路徑(不過濾特殊字符),由于handle.exe對于中文路徑或文件名兼容不好,返回的數據存在?或其它亂碼字符,如查采用:Path.Combine方法則會報錯,故這里自定義一個方法,只是簡單的拼接
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 | /// <summary> /// 拼接路徑(不過濾殊字符) /// </summary> /// <param name="paths"></param> /// <returns></returns> private ?string ?PathJoin( params ?string [] paths) { ???? if ?(paths ==? null ?|| paths.Length <= 0) ???? { ???????? return ?string .Empty; ???? } ???? string ?newPath = paths[0]; ???? for ?( int ?i = 1; i < paths.Length; i++) ???? { ???????? if ?(!newPath.EndsWith( "\\" )) ???????? { ???????????? newPath +=? "\\" ; ???????? } ???????? if ?(paths[i].StartsWith( "\\" )) ???????? { ???????????? paths[i] = paths[i].Substring(1); ???????? } ???????? newPath += paths[i]; ???? } ???? return ?newPath; } |
CloseProcessWithFile:核心方法,關閉指定文件被占用的進程,上述所有的方法均是為了實現該方法的功能
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 | private ?void ?CloseProcessWithFile( string ?filePath) { ???? if ?(!IsFileUsing(filePath))? return ; ???? ShowDownInfo( string .Format( "正在嘗試解除占用文件 {0}" , _FilePaths[_FileIndex])); ???? var ?runProcInfos = GetRunProcessInfos(filePath);? //獲取被占用的進程 ???? System.IO.File.WriteAllText(Path.Combine(Application.StartupPath,? "runProcInfos.txt" ),? string .Join( "\r\n" , runProcInfos.Select(p =>? string .Format( "ProdId:{0},ProcName:{1}" , p.Key, p.Value)).ToArray())); //DEBUG用,正式發布時可以去掉 ???? var ?localProcesses = Process.GetProcesses(); ???? bool ?hasKilled =? false ; ???? foreach ?( var ?item? in ?runProcInfos) ???? { ???????? if ?(item.Key != currentProcessId)? //排除當前進程 ???????? { ???????????? var ?runProcess = localProcesses.SingleOrDefault(p => p.Id == item.Key); ???????????? //var runProcess = Process.GetProcessById(item.Key); ???????????? if ?(runProcess !=? null ) ???????????? { ???????????????? try ???????????????? { ???????????????????? runProcess.Kill();? //強制關閉被占用的進程 ???????????????????? hasKilled =? true ; ???????????????? } ???????????????? catch ???????????????? { } ???????????? } ???????? } ???? } ???? if ?(hasKilled) ???? { ???????? Thread.Sleep(500); ???? } } |
上述代碼邏輯簡要說明:先判斷是否被占用,若被占用,則獲取該文件被占用的進程列表,然后獲取一下當前操作系統的所有進程列表,最后通過進程ID查詢得到排除當前程序自己的進程ID(currentProcessId = Process.GetCurrentProcess().Id)列表,若能獲取得到,表明進程仍在運行,則強制終止該進程,實現解除文件占用
注意:KILL掉占用進程后,可能由于緩存原因,若直接進行文件的覆蓋與替換或轉移操作,可能仍會報錯,故這里作了一個判斷,若有成功KILL掉進程,則需等待500MS再去做更新文件之類的操作;
本文轉自 夢在旅途 博客園博客,原文鏈接:http://www.cnblogs.com/zuowj/p/5840567.html??,如需轉載請自行聯系原作者