如何最好地监控桌面应用程序?

29

我需要一种监控桌面应用程序并在其死亡时重新启动的方法。

最初我认为最好的方法是从Windows服务监视/重新启动进程,直到我发现自Vista以来Windows服务不应与桌面交互

我看过几个处理这个问题的问题,但每个答案都涉及某种被微软反对的黑客行为,很可能会停止在未来的操作系统更新中工作。

因此,Windows服务现在可能不是一个选择。我可以创建一个不同的桌面/控制台应用程序来执行此操作,但这有点违背了它的目的。

在您的意见中,哪种方式最优雅地实现这一点?

编辑:这既不是恶意软件也不是病毒。需要监视的应用程序是在嵌入式系统上运行的媒体播放器,尽管我正在尝试覆盖所有可能的崩溃情况,但我不能冒险因意外错误而导致其崩溃(事情总是无法预料)。这个看门狗只是在其他所有内容出错的情况下保护措施。此外,由于播放器将显示第三方Flash内容,另一个加分的功能可能是监视资源使用情况,并在例如一些垃圾Flash电影开始泄漏内存时重新启动播放器。

编辑2:我忘记提到,我想要监视/重新启动的应用程序绝对不需要在LocalSystem帐户上运行,也不需要任何管理权限。实际上,我希望它使用当前登录用户凭据运行。


9
为什么这听起来像是恶意软件或病毒? - SliverNinja - MSFT
4
确保另一个程序始终运行的程序是恶意程序的迹象。服务已经存在以涵盖大多数合法的使用情况。 - Servy
4
也许你想知道如何在应用程序崩溃后重新启动它:https://dev59.com/93RA5IYBdhLWcg3w8SaJ - Richard Morgan
25
如果答案可能被用于恶意软件/病毒,那么提出编程问题就是错误的吗?也许Stack Overflow应该要求任何提问者提供证据,证明他们的问题与恶意软件无关?这就是我一直在Stack Overflow上感到困扰的事情,如果你的问题即使稍微引起了恶意软件的联想,你就必须为自己辩护并说服别人你不是在写病毒。那“无罪推定”呢?看看你的评论受到了多少赞! - Andrew Savinykh
10
@Gabe: 我和您一样是社区成员。我的观点是,如果您的道德或道德准则阻止您回答问题,那么可以理解。然而,如果您暗示一个问题可能有恶意,而您唯一的证据是您的“直觉”,那对问题提出者来说就是不礼貌的。我不打算在这里继续讨论此事,很抱歉提及此事,这不是合适的地方。如果您有兴趣进一步讨论,请随时在 Meta 上提出问题并在此处链接。谢谢。 - Andrew Savinykh
显示剩余8条评论
4个回答

19

我最终实现了@A_nto2建议的解决方案,并且它完全达到了我想要的效果:我现在有一个Windows服务来监视一组进程,每当它们关闭时,就会自动使用活动用户的凭据和会话重新启动它们,以便GUI可见。

但是,由于他发布的链接显示VC ++代码,所以我分享我的C#实现,供遇到相同问题的任何人参考:

public static class ProcessExtensions
{
    public enum SECURITY_IMPERSONATION_LEVEL
    {
        SecurityAnonymous,
        SecurityIdentification,
        SecurityImpersonation,
        SecurityDelegation
    }

    [StructLayout(LayoutKind.Sequential)]
    public class SECURITY_ATTRIBUTES
    {
        public int nLength;
        public IntPtr lpSecurityDescriptor;
        public int bInheritHandle;
    }

    public enum TOKEN_TYPE
    {
        TokenPrimary = 1,
        TokenImpersonation
    }

    [Flags]
    public enum CREATE_PROCESS_FLAGS : uint
    {
        NONE = 0x00000000,
        DEBUG_PROCESS = 0x00000001,
        DEBUG_ONLY_THIS_PROCESS = 0x00000002,
        CREATE_SUSPENDED = 0x00000004,
        DETACHED_PROCESS = 0x00000008,
        CREATE_NEW_CONSOLE = 0x00000010,
        NORMAL_PRIORITY_CLASS = 0x00000020,
        IDLE_PRIORITY_CLASS = 0x00000040,
        HIGH_PRIORITY_CLASS = 0x00000080,
        REALTIME_PRIORITY_CLASS = 0x00000100,
        CREATE_NEW_PROCESS_GROUP = 0x00000200,
        CREATE_UNICODE_ENVIRONMENT = 0x00000400,
        CREATE_SEPARATE_WOW_VDM = 0x00000800,
        CREATE_SHARED_WOW_VDM = 0x00001000,
        CREATE_FORCEDOS = 0x00002000,
        BELOW_NORMAL_PRIORITY_CLASS = 0x00004000,
        ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000,
        INHERIT_PARENT_AFFINITY = 0x00010000,
        INHERIT_CALLER_PRIORITY = 0x00020000,
        CREATE_PROTECTED_PROCESS = 0x00040000,
        EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
        PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000,
        PROCESS_MODE_BACKGROUND_END = 0x00200000,
        CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
        CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
        CREATE_DEFAULT_ERROR_MODE = 0x04000000,
        CREATE_NO_WINDOW = 0x08000000,
        PROFILE_USER = 0x10000000,
        PROFILE_KERNEL = 0x20000000,
        PROFILE_SERVER = 0x40000000,
        CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000,
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct STARTUPINFO
    {
        public Int32 cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public Int32 dwX;
        public Int32 dwY;
        public Int32 dwXSize;
        public Int32 dwYSize;
        public Int32 dwXCountChars;
        public Int32 dwYCountChars;
        public Int32 dwFillAttribute;
        public Int32 dwFlags;
        public Int16 wShowWindow;
        public Int16 cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public int dwProcessId;
        public int dwThreadId;
    }

    public class Kernel32
    {
        [DllImport("kernel32.dll", EntryPoint = "WTSGetActiveConsoleSessionId")]
        public static extern uint WTSGetActiveConsoleSessionId();

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool CloseHandle(IntPtr hObject);
    }

    public class WtsApi32
    {
        [DllImport("Wtsapi32.dll", EntryPoint = "WTSQueryUserToken")]
        public static extern bool WTSQueryUserToken(UInt32 sessionId, out IntPtr phToken);
    }

    public class AdvApi32
    {
        public const uint MAXIMUM_ALLOWED = 0x2000000;

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public extern static bool DuplicateTokenEx
        (
            IntPtr hExistingToken,
            uint dwDesiredAccess,
            SECURITY_ATTRIBUTES lpTokenAttributes,
            SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
            TOKEN_TYPE TokenType,
            out IntPtr phNewToken
        );

        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool CreateProcessAsUser
        (
            IntPtr hToken,
            string lpApplicationName,
            string lpCommandLine,
            SECURITY_ATTRIBUTES lpProcessAttributes,
            SECURITY_ATTRIBUTES lpThreadAttributes,
            bool bInheritHandles,
            CREATE_PROCESS_FLAGS dwCreationFlags,
            IntPtr lpEnvironment,
            string lpCurrentDirectory,
            ref STARTUPINFO lpStartupInfo,
            out PROCESS_INFORMATION lpProcessInformation
        );
    }

    public class UserEnv
    {
        [DllImport("userenv.dll", SetLastError = true)]
        public static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);

        [DllImport("userenv.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);
    }

    public static void StartAsActiveUser(this Process process)
    {
        // Sanity check.
        if (process.StartInfo == null)
        {
            throw new InvalidOperationException("The StartInfo property must be defined");
        }

        if (string.IsNullOrEmpty(process.StartInfo.FileName))
        {
            throw new InvalidOperationException("The StartInfo.FileName property must be defined");
        }

        // Retrieve the active session ID and its related user token.
        var sessionId = Kernel32.WTSGetActiveConsoleSessionId();
        var userTokenPtr = new IntPtr();
        if (!WtsApi32.WTSQueryUserToken(sessionId, out userTokenPtr))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        // Duplicate the user token so that it can be used to create a process.
        var duplicateUserTokenPtr = new IntPtr();
        if (!AdvApi32.DuplicateTokenEx(userTokenPtr, AdvApi32.MAXIMUM_ALLOWED, null, SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, TOKEN_TYPE.TokenPrimary, out duplicateUserTokenPtr))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        // Create an environment block for the interactive process.
        var environmentPtr = new IntPtr();
        if (!UserEnv.CreateEnvironmentBlock(out environmentPtr, duplicateUserTokenPtr, false))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        // Create the process under the target user’s context.
        var processFlags = CREATE_PROCESS_FLAGS.NORMAL_PRIORITY_CLASS | CREATE_PROCESS_FLAGS.CREATE_NEW_CONSOLE | CREATE_PROCESS_FLAGS.CREATE_UNICODE_ENVIRONMENT;
        var processInfo = new PROCESS_INFORMATION();
        var startupInfo = new STARTUPINFO();
        startupInfo.cb = Marshal.SizeOf(startupInfo);
        if (!AdvApi32.CreateProcessAsUser
        (
            duplicateUserTokenPtr, 
            process.StartInfo.FileName, 
            process.StartInfo.Arguments, 
            null, 
            null, 
            false, 
            processFlags, 
            environmentPtr, 
            null, 
            ref startupInfo, 
            out processInfo
        ))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        // Free used resources.
        Kernel32.CloseHandle(processInfo.hProcess);
        Kernel32.CloseHandle(processInfo.hThread);
        if (userTokenPtr != null)
        {
            Kernel32.CloseHandle(userTokenPtr);
        }

        if (duplicateUserTokenPtr != null)
        {
            Kernel32.CloseHandle(duplicateUserTokenPtr);
        }

        if (environmentPtr != null)
        {
            UserEnv.DestroyEnvironmentBlock(environmentPtr);
        }
    }
}

这是代码如何被调用的示例:

var process = new Process();
process.StartInfo = new ProcessStartInfo { FileName = @"C:\path-to\target.exe", Arguments = "-arg1 -arg2" };
process.StartAsActiveUser();

希望对您有所帮助!


3
对于将来使用此代码的任何人:它非常有效,但服务必须作为LocalSystem运行。 - Kevin Coulombe
支持哪个Windows版本? - Yazan Jaber
它应该在Vista和7下正常工作。没有测试其他的操作系统。如果你正在运行一个较旧的操作系统(例如XP),你不需要这段代码,因为Windows服务当时能够显示GUI界面。 - Axel Magagnini
你是否在应用程序崩溃/退出后也重新启动了它?我正在尝试实现它,但似乎失败了。 - Lonefish
2
为什么在WTSQueryUserToken()时会出现错误?错误信息:尝试引用不存在的令牌。我的服务正在以本地系统身份运行。 - juicebyjustin

6

一开始我认为最好的方法是通过Windows服务监控/重启进程...

当然可以!我以前也做过。 你可以通过观看这个视频来开始学习:

http://msdn.microsoft.com/en-us/windows7trainingcourse_win7session0isolation_topic2#_Toc243675529

和这个:

http://www.codeproject.com/Articles/18367/Launch-your-application-in-Vista-under-the-local-s

实际上,您需要使用当前用户的SessionID作为SYSTEM运行程序。

如果您感到懒惰,我想可能会有一些很好的小服务可以满足您的需求。尝试在www.codeproject.com上搜索。


抱歉,我链接的是C++代码...但你可以很容易地找到C#版本。 - A_nto2
我刚刚分享了我的C#实现作为另一种答案,以防其他人遇到相同的问题。 - Axel Magagnini

5
看门狗进程可以利用 System.Diagnostics.Process 来启动应用程序,使用 WaitForExitMethod() 并检查 ExitCode 属性。
针对问题的抱怨,我不得不在处理一个没有源代码控制访问权限的旧呼叫中心应用程序时使用这种方法。
编辑:
对于主机应用程序,您可以使用“Windows 应用程序”输出类型的 .NET 应用程序,并且根本不需要表单。例如:
namespace WindowsFormsApplication1
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            var info = new ProcessStartInfo(@"calc.exe");
            var process = Process.Start(info);
            process.WaitForExit();
            MessageBox.Show("Hello World!");
        }
    }
}

这差不多就是我尝试过的,但被监控的应用程序有一个GUI,所以这种方法在Vista及以上版本中行不通。 - Axel Magagnini
@AxelMagagnini - 为什么它不起作用。他没有建议在Windows服务中执行此操作。 - Chris Dunaway
@ChrisDunaway 我以为他是这个意思,如果我误解了就对不起啦。那么,你会从哪里开始呢? - Axel Magagnini
是的,我忽略了Windows服务方面! - oasten

0
发现了这个库,它是在Code Project上编写的: https://www.codeproject.com/Tips/1054098/Simple-Csharp-Watchdog 它是在此处最新答案发布3年后发布的,因此为了记录而添加。
-- 附言: 我们在应用程序中安装了它,它运行得非常好。需要稍微调整以支持我们的用例,但代码相当稳定和直观。

我没有尝试过这个,但从外观上看,它解决了一个略微不同的问题。我将允许您的应用程序启动和监视进程,但由于原始帖子中提到的限制,它无法作为Windows服务运行。它使用与@oasten答案相同的方法。 - Axel Magagnini
尝试后更新了答案。无论如何,你是正确的——从你最初的问题中看来,自 Windows Vista 以来,服务似乎不太适用。这段代码将创建另一个进程来保持应用程序的运行(它还有交叉检查以保持看门狗的运行,以及心跳机制)。仅供参考。@oasten 的答案肯定类似,只是更加“代码完整”。 - Oded Ben Dov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接