c#+handle.exe实现升级程序在运行时自动解除文件被占用的问题

简介:

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

相关文章
|
3月前
|
存储 安全 Java
程序与技术分享:C#值类型和引用类型的区别
程序与技术分享:C#值类型和引用类型的区别
29 0
|
7天前
|
C# 容器
C#中的命名空间与程序集管理
在C#编程中,`命名空间`和`程序集`是组织代码的关键概念,有助于提高代码的可维护性和复用性。本文从基础入手,详细解释了命名空间的逻辑组织方式及其基本语法,展示了如何使用`using`指令访问其他命名空间中的类型,并提供了常见问题的解决方案。接着介绍了程序集这一.NET框架的基本单位,包括其创建、引用及高级特性如强名称和延迟加载等。通过具体示例,展示了如何创建和使用自定义程序集,并提出了针对版本不匹配和性能问题的有效策略。理解并善用这些概念,能显著提升开发效率和代码质量。
22 4
|
13天前
|
Linux C# 开发者
Uno Platform 驱动的跨平台应用开发:从零开始的全方位资源指南与定制化学习路径规划,助您轻松上手并精通 C# 与 XAML 编程技巧,打造高效多端一致用户体验的移动与桌面应用程序
【9月更文挑战第8天】Uno Platform 的社区资源与学习路径推荐旨在为初学者和开发者提供全面指南,涵盖官方文档、GitHub 仓库及社区支持,助您掌握使用 C# 和 XAML 创建跨平台原生 UI 的技能。从官网入门教程到进阶技巧,再到活跃社区如 Discord,本指南带领您逐步深入了解 Uno Platform,并提供实用示例代码,帮助您在 Windows、iOS、Android、macOS、Linux 和 WebAssembly 等平台上高效开发。建议先熟悉 C# 和 XAML 基础,然后实践官方教程,研究 GitHub 示例项目,并积极参与社区讨论,不断提升技能。
32 2
|
1月前
|
监控 安全 C#
使用C#如何监控选定文件夹中文件的变动情况?
使用C#如何监控选定文件夹中文件的变动情况?
94 19
|
1月前
|
编译器 C# Windows
C#基础:手动编译一个.cs源代码文件并生成.exe可执行文件
通过上述步骤,应该能够高效准确地编译C#源代码并生成相应的可执行文件。此外,这一过程强调了对命令行编译器的理解,这在调试和自动化编译流程中是非常重要的。
73 2
|
1月前
|
文字识别 C# Python
使用C#将几个Excel文件合并去重分类
使用C#将几个Excel文件合并去重分类
21 3
|
27天前
|
缓存 NoSQL Redis
【Azure Redis 缓存】C#程序是否有对应的方式来优化并缩短由于 Redis 维护造成的不可访问的时间
【Azure Redis 缓存】C#程序是否有对应的方式来优化并缩短由于 Redis 维护造成的不可访问的时间
|
1月前
|
安全 C# 开发者
【C# 多线程编程陷阱揭秘】:小心!那些让你的程序瞬间崩溃的多线程数据同步异常问题,看完这篇你就能轻松应对!
【8月更文挑战第18天】多线程编程对现代软件开发至关重要,特别是在追求高性能和响应性方面。然而,它也带来了数据同步异常等挑战。本文通过一个简单的计数器示例展示了当多个线程无序地访问共享资源时可能出现的问题,并介绍了如何使用 `lock` 语句来确保线程安全。此外,还提到了其他同步工具如 `Monitor` 和 `Semaphore`,帮助开发者实现更高效的数据同步策略,以达到既保证数据一致性又维持良好性能的目标。
28 0
|
1月前
|
C# 图形学 数据安全/隐私保护
Unity数据加密☀️ 二、使用Rider将C#代码生成DLL文件
Unity数据加密☀️ 二、使用Rider将C#代码生成DLL文件