65.9K
CodeProject 正在变化。 阅读更多。
Home

保持进程活动或通过即时重启防止应用程序终止

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (10投票s)

2013年10月23日

CPOL

6分钟阅读

viewsIcon

70541

downloadIcon

4235

如果您想用一种简单的方法来保持您的应用程序存活,您可能会对这种方法感兴趣。而且请记住,不可杀死的进程通常是一种糟糕的设计,在大多数情况下,您基本上不应该使用它。

您可以在 GitHub 上找到源代码。 

介绍 

首先,您应该考虑是否真的需要让您的应用程序或进程不可杀死/不可终止。坦率地说,在大多数情况下,这不过是一种糟糕的设计,用户会因此想要杀死您。

如果您有这样的需求,最好深入了解细节,找出它所涵盖的真实场景,并使用最佳实践来实现它。 

为什么会这样?操作系统为您提供了丰富的 API,因此您确实可以实现某种不可终止的进程,但有时这并不受支持,最终您的应用程序将充满错误,甚至被防病毒软件阻止。原生场景可能包括:

  • Windows 服务
  • 安全权限 
  • 调试器 API
  • 其他   

其他场景

  • 进程命名技巧 
  • 保持进程存活(本文讨论的主题)
  • 用户模式挂钩(带有加载的 DLL 或注入的线程)
  • 内核模式调用挂钩
  • 直接内核对象操作 (DKOM) 这个非常酷,但看起来像真正的安全漏洞,这就是为什么几乎所有的杀毒软件都会发现此类应用程序被感染。 您可以在此处找到描述、源代码和可执行文件。 
  • 使用调试器 API。 
  • 工具操作,当您针对特定工具进行“修补”以保护您的进程时。

您可以在此处找到有关这些方法的更多信息。.

在本文中,我将介绍保持进程存活的解决方案。我建议不要在实际项目中使用它,因为它可能会带来错误、不可预测的行为和不稳定性。但当然,您可以通过自己的方式处理这些事情,请参阅“可能的改进”部分。 

功能

此解决方案包括:

  • 主(客户端)进程,在退出时由辅助(保持存活)进程立即重启
  • 辅助(保持存活)进程,在终止时被重启 

客户端进程是单实例应用程序,而辅助进程是多实例。   

 

用户启动客户端应用程序,终止它,然后终止保持存活进程,然后退出应用程序。 

首先,保持存活进程具有运行客户端应用程序或其自身的并通过退出来防止用户终止进程树的功能。当您启动一个启动另一个进程的进程时,您可以几乎同时终止它们,例如通过在任务管理器中单击父进程并选择“结束进程树”。但是,当您在中间有一个立即启动另一个进程后退出的进程来中断父子关系时,您就阻止了用户终止进程树。因此,当我谈论进程启动另一个进程时,我指的是在中间进程的帮助下启动它,该中间进程会中断父子关系。 

当用户启动客户端应用程序时,如果它未运行,它将启动保持存活进程。保持存活进程接收进程 ID 并等待具有该 ID 的进程退出。当它退出时,保持存活进程会立即重启它。客户端进程也是如此,它在单独的线程中等待保持存活进程退出。当保持存活进程退出时,客户端应用程序会重新启动它。

要退出,您可以定义一个标志来打破重启循环,并在您的退出事件发生时设置它。另外,您可能希望防止用户频繁重启进程,以防不稳定的代码在启动时崩溃应用程序。在这种情况下,应用程序将崩溃并立即重启,而我们陷入了一个循环。为了处理这种情况,保持存活进程会检查它在过去 10 分钟内是否重启了客户端应用程序超过 10 次,如果是这样,保持存活进程将停止重启并退出。 

此解决方案允许您: 

  • 立即重启被用户通过任务管理器(结束进程)终止的应用程序。
  • 如果用户杀死了您应用程序的进程树(结束进程树),立即重启您的应用程序。
  • 使保持存活进程和客户端进程都可重启。

解决方案的缺点

  • 它并不可靠,如果您编写了一个应用程序可以在几乎同时杀死给定进程(我没有尝试过,但应该是这样)或者在重启时删除可执行文件,那么您就可以杀死进程,但它仍然适用于某些任务(请参阅可能的用法)。
  • 在慢速机器上的冷启动 - 会给用户一些时间来终止客户端进程。
  • 在开发过程中可能会不稳定,一个小错误可能导致重启循环和其他有趣的现象。

测试期间如何实际关闭它们。

  1. 在控制台窗口中输入 exit,然后按 Enter。
  2. 使用包中的 SimultaneousTerminate 工具 
  3. 由于此方法不对线程挂起做任何处理,因此您可以挂起“KeepAlive”和“KeepAliveClient”进程,然后杀死它们(例如,使用 SysInternals ProcessExplorer)。
  4. 尝试在反复使用任务管理器/ProcessExplorer 杀死进程时删除可执行文件。
  5. 编写一个应用程序,尝试几乎同时杀死选定的进程。

可能的用法和改进 

用法: 

  • 您希望在进程失败并终止时重新启动它。因此,我们在这里处理的是一个不稳定的应用程序,我们希望在它失败时重新启动它。
  • 您想阻止用户使用任务管理器终止它。任务管理器不会在不让新的进程创建的情况下终止两者。  

可能的改进: 

  • 您可以检查进程是否被挂起并恢复它,或者启动另一个进程。 
  • 错误处理。有很多地方可以添加空检查或 try/catch 块,因为对象的状态可能会非常快地改变,所以最好添加适当的异常处理。
  • 保持存活不会影响操作系统关机,但是为了在操作系统关机时优雅退出,您可能希望通过系统事件检测到它并绕过保持存活代码。
  • 用原生代码重写辅助(保持存活)进程 :) 它非常小巧简单,所以我们可以稍微提高它的性能。
  • 还有更多...  

使用代码 

解决方案包括三个项目:客户端应用程序(您希望使其自动可重启),保持存活应用程序,该应用程序具有中断父子进程关系并保持客户端存活的功能,以及“Simultaneous Terminate”,它以一种不允许进程重启的方式终止进程。

要测试功能,您可以运行 KeepAliveClient,尝试使用任务管理器终止它或 KeepAlive 进程。要退出,请输入 'exit'  然后按 Enter。 

客户端应用程序代码 

这是一个单实例应用程序,因此使用 Mutex 来保证这一点。应用程序接受参数来查找启动了客户端应用程序的保持存活进程。如果客户端应用程序是由用户启动的,则不提供任何参数,因此 `_processId` 将为  0。 

解析参数后,如果有参数,则在单独的线程中启动 `KeepingAlive` 方法。该方法执行以下操作:

  • 启动保持存活进程
  • 等待保持存活进程退出,以便重新启动它
  • 使用中间进程运行保持存活进程(以中断父子进程关系)  
  • 如果设置了 `_exiting` 标志,则中断重启循环  
public class Program
{
    private const string KeepAlive = "KeepAlive.exe";
    private static Process _keepAliveProcess;
    private static Mutex _instanceMutex;
    private static bool _exiting;
    private static int _processId;

    [STAThread]
    public static void Main(string[] args)
    {
        if (!SingleInstance())
            return;

        if (args.Length > 1 && String.Equals(args[0], "keepaliveid"))
            Int32.TryParse(args[1], out _processId);

        var thread = new Thread(KeepingAlive);
        thread.Start();

        while (true)
        {
            if (Console.ReadLine() != "exit") 
                continue;
            _exiting = true;
            ReleaseSingleInstance();
            _keepAliveProcess.Kill();
            Environment.Exit(0);
        }
    }
        
    private static void KeepingAlive()
    {
        while (true)
        {
            if (_exiting)
                return;

            if (_processId == 0)
            {
                var kamikazeProcess = Process.Start(KeepAlive, 
                  string.Concat("launchselfandexit ", Process.GetCurrentProcess().Id));
                if (kamikazeProcess == null)
                    return;
                    
                kamikazeProcess.WaitForExit();
                _keepAliveProcess = Process.GetProcessById(kamikazeProcess.ExitCode);
            }
            else
            {
                _keepAliveProcess = Process.GetProcessById(_processId);
                _processId = 0;
            }
                
            _keepAliveProcess.WaitForExit();
        }
    }

    private static bool SingleInstance()
    {
        bool createdNew;
        _instanceMutex = new Mutex(true, @"Local\4A31488B-F86F-4970-AF38-B45761F9F060", out createdNew);
        if (createdNew) return true;
        Debug.WriteLine("Application already launched. Shutting down.");
        _instanceMutex = null;
        return false;
    }

    private static void ReleaseSingleInstance()
    {
        if (_instanceMutex == null)
            return;
        _instanceMutex.ReleaseMutex();
        _instanceMutex.Close();
        _instanceMutex = null;
    }
} 

保持存活应用程序代码

保持存活具有启动客户端应用程序或自身并退出的功能。这基本上使我们能够中断父子进程关系。 

当客户端启动保持存活时,它会通过参数提供其进程 ID,因此保持存活知道它应该等待哪个进程退出。如果没有运行客户端(它已退出/被终止),保持存活将启动它。 

还有一个功能可以防止频繁重启。如果在 10 分钟内应用程序退出了/被终止了 10 次,它将不再被启动。 

public class Program
{
    private const string KeepAlive = "KeepAlive.exe";
    private const string KeepAliveClient = "KeepAliveClient.exe";
    private static readonly SortedSet<DateTime> RestartHistory = new SortedSet<DateTime>();

    [STAThread]
    public static void Main(string[] args)
    {
        if (args.Length < 2)
            return;
        var action = args[0];

        if (action.Equals("launchclientandexit", StringComparison.OrdinalIgnoreCase))
        {
            LaunchAndExit(KeepAliveClient, string.Concat("keepaliveid ", args[1]));
            return;
        }

        if (action.Equals("launchselfandexit", StringComparison.OrdinalIgnoreCase))
        {
            LaunchAndExit(KeepAlive, string.Concat("clientid ", args[1]));
            return;
        }

        if (action.Equals("clientid", StringComparison.OrdinalIgnoreCase))
        {
            var processId = 0;
            Int32.TryParse(args[1], out processId);
            if (processId > 0) 
                KeepingAlive(processId);
        }
    }

    private static void LaunchAndExit(string fileName, string parameters)
    {
        var process = Process.Start(fileName, parameters);
        if (process != null) Environment.Exit(process.Id);
    }

    private static void KeepingAlive(int processId)
    {
        while (true)
        {
            Process keepAliveProcess;
            if (processId == 0)
            {
                var kamikazeProcess = Process.Start(KeepAlive, 
                  string.Concat("launchclientandexit ", Process.GetCurrentProcess().Id));
                if (kamikazeProcess == null)
                    return;
                kamikazeProcess.WaitForExit();
                keepAliveProcess = Process.GetProcessById(kamikazeProcess.ExitCode);
            }
            else
            {
                keepAliveProcess = Process.GetProcessById(processId);
                processId = 0;
            }
            keepAliveProcess.WaitForExit();
                
            // If client failed more than 10 time within a 10 minutes stop restarting it
            var time = DateTime.Now;
            RestartHistory.Add(time);
            while (RestartHistory.Count > 0 && 
                    (RestartHistory.Min - time) > TimeSpan.FromMinutes(10))
                RestartHistory.Remove(RestartHistory.Min);
            if (RestartHistory.Count >= 10)
                return;
        }
    }
} 

历史

  • 2013/10/23 - 初始版本。
© . All rights reserved.