本文共 7836 字,大约阅读时间需要 26 分钟。
我公司最近升级程序经常报出更新失败问题,究其原因,原来是更新时,他们可能又打开了正在被更新的文件,导致更新文件时,文件被其它进程占用,无法正常更新而报错,为了解决这个问题,我花了一周时间查询多方资料及研究,终于找到了一个查询进程的利器:handle.exe,下载地址:,我是通过它来找到被占用的进程,然后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 ,如需转载请自行联系原作者