What's the best way to watchdog a desktop application?
Asked Answered
K

4

29

I need some way to monitor a desktop application and restart it if it dies.

Initially I assumed the best way would be to monitor/restart the process from a Windows service, until I found out that since Vista Windows services should not interact with the desktop

I've seen several questions dealing with this issue, but every answer I've seen involved some kind of hack that is discouraged by Microsoft and will likely stop working in future OS updates.

So, a Windows service is probably not an option anymore. I could probably just create a different desktop/console application to do this, but that kind of defeats its purpose.

Which would be the most elegant way to achieve this, in your opinion?

EDIT: This is neither malware nor virus. The app that needs monitoring is a media player that will run on an embedded system, and even though I'm trying to cover all possible crash scenarios, I can't risk having it crash over an unexpected error (s**t happens). This watchdog would be just a safeguard in case everything else goes wrong. Also, since the player would be showing 3rd party flash content, an added plus would be for example to monitor for resource usage, and restart the player if say, some crappy flash movie starts leaking memory.

EDIT 2: I forgot to mention, the application I would like to monitor/restart has absolutely no need to run on either the LocalSystem account nor with any administrative privileges at all. Actually, I'd prefer it to run using the currently logged user credentials.

Kramlich answered 21/6, 2012 at 20:40 Comment(12)
Cant think of anything else other than a process that monitors..Mia
creating a program that ensures another program is always running is a sign of a malicious program. Services are there to cover most all of the legitimate use cases.Concertmaster
The most elegant thing to do would be to not do it. Write a program so good it won't crash and your users won't want to kill it!Buttonball
Perhaps you are asking how to restart an application if it dies: #779905Henke
@SliverNinja: so it's wrong to ask a programming question, if the answer could be used for malware/virus purposes? May be SO should require proof from any question write that the question is not related to malware? This is what always bugged me on SO, if you question ever so slightly might evoke a notion of malware you have to defend yourself and convince that you didn't mean writing a virus. What about innocent until proven guilty? Just look at all these upvotes on your comment!Snaffle
A) What is the purpose of the program to always be running? Why wouldn't a service do the trick? B) A service that monitors for a program and starts it if it closes is not interacting with the Desktop, what that means is that the service should not the require additional info from the user to complete tasks and should run as if it wasn't thereChemoreceptor
It's neither malware nor virus. The app that needs monitoring is a media player that will run on an embedded system, and even though I'm trying to cover all possible crash scenarios, I can't risk having it crash over an unexpected error (s**t happens). This watchdog would be just a safeguard in case everything else goes wrong. Also, since the player would be showing 3rd party flash content, an added plus would be for example to monitor for resource usage, and restart the player if say, some crappy flash movie starts leaking memory.Kramlich
@RichardMorgan: Locally. The process would run on the same host as the watchdog. I'm looking at the link you posted, but so far it looks like a hacky solution. Thanks anyway!Kramlich
@zespri - There is a sense of ethics on SO. If a member should think the OP is for malicious intent, they're allowed to voice that concern. Who are you to tell them not to? The OP can clarify the question to clear up any doubts and move forward. Relax.Dianadiandra
I actually didn't even think about the possible malicious uses of this, but just edited the question to clarify the intended usage.Kramlich
@Gabe: I'm a member of this community not unlike yourself. My opinion is if your ethics or morale prohibit you from answering questions, that's fine. However to insinuate that a question might have a malicious intent, when all proof that you have is your "gut feeling" is just impolite to the OP. I'm not going to continue discussing this here, and I'm sorry for bringing it up, it's not the place. If you are interested in further discussion, please feel free to open a question on meta and link it here. Thanks.Snaffle
@zespri - Congratulations on being a member of SO. If a member feels the need to question the intent of an OP, they're allowed to do so, hence the comments. I am sorry if you can't handle the devil's advocate concept, perhaps some more experience will help you understand.Dianadiandra
R
6

Initially I assumed the best way would be to monitor/restart the process from a Windows service...

Sure you can! I did it some times ago. You can start learning how watching this:

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

and this:

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

In substance, you have to run programs as SYSTEM, but with the SessionID of the current user.

If you're feeling lazy, I suppose there could be some good little Services which make the thing you're looking for. Try searching on www.codeproject.com.

Rabiah answered 21/6, 2012 at 21:6 Comment(2)
Sorry, I've linked to C++ codes...but you could easily find the c# flavours.Rabiah
I have just shared my C# implementation as an alternative answer in case anyone else is facing the same issue.Kramlich
K
19

I finally implemented a the solution suggested by @A_nto2 and it achieved exactly what I was looking for: I now have a Windows Service that monitors a list of processes and whenever they are down, they are launched again automatically using the active user's credentials and session, so the GUI is visible.

However, since the links he posted shown VC++ code, I'm sharing my C# implementation for anyone dealing with the same issue:

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);
        }
    }
}

And here's how the code is invoked:

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

Hope it helps!

Kramlich answered 29/6, 2012 at 18:19 Comment(5)
For anyone using this code in the future : it works very well, but the service MUST run as LocalSystem.Insular
What windows version is supported?Wide
It should work fine under Vista and 7. Haven't tested any others. If you're running an older OS (XP for example) you shouldn't need this code, because Windows Services were able to show a GUI back then.Kramlich
Did you also restart the application on a crash/exit? I'm trying to implement it, but seem to be failing.Souvenir
Why would I be getting an error at WTSQueryUserToken(). Error: an attempt was made to reference a token that does not exists. My service is running as local system.Encephalograph
R
6

Initially I assumed the best way would be to monitor/restart the process from a Windows service...

Sure you can! I did it some times ago. You can start learning how watching this:

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

and this:

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

In substance, you have to run programs as SYSTEM, but with the SessionID of the current user.

If you're feeling lazy, I suppose there could be some good little Services which make the thing you're looking for. Try searching on www.codeproject.com.

Rabiah answered 21/6, 2012 at 21:6 Comment(2)
Sorry, I've linked to C++ codes...but you could easily find the c# flavours.Rabiah
I have just shared my C# implementation as an alternative answer in case anyone else is facing the same issue.Kramlich
I
5

The watchdog process could make use of System.Diagnostics.Process to launch the application, use the WaitForExitMethod() and check the ExitCode property.

In response to the complaints over the question, I have had to use such a method when working with a legacy call center application over which I had no source control access.

EDIT:

For the host application you could use a .NET application of output type "Windows Application" and simply not have a form at all. For example:

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!");
        }
    }
}
Interdigitate answered 21/6, 2012 at 21:1 Comment(4)
That's more or less exactly what I tried, but the application being monitored has a GUI, so this method won't work from Vista and up.Kramlich
@AxelMagagnini - Why won't it work. He didn't suggest doing this in a Windows Service.Santinasantini
@ChrisDunaway I assumed he meant that, sorry if I misunderstood. And if so, from where would you do it then?Kramlich
Yes, I neglected the Windows Service aspect!Interdigitate
C
0

Found this lib written up on Code Project: https://www.codeproject.com/Tips/1054098/Simple-Csharp-Watchdog

It was posted 3 years after the latest answer here, so adding it for record's sake.

-- Addendum: Installed it in our app, and it works pretty well. Needed slight tweaking to support our use case, but the code is pretty solid and straight forward

Contradance answered 24/6, 2019 at 5:39 Comment(2)
I haven't tried this but from the looks of it, it solves a slightly different problem. I will just allow your application to start and monitor processes, but it won't work as a Windows service because of the limitations mentioned in the original post. It uses the same method as the answer from @oasten.Kramlich
Updated the answer after trying it. In any case, you are right - it's not a service and from your original question it seems a service is not suitable since Windows Vista. This code will make another process that keeps your app alive (and it has a cross-check to keep the watchdog alive, as well as a heartbeat mechanism). Just FYI. @Interdigitate answer is definitely similar, this is just a bit more "code complete"Contradance

© 2022 - 2024 — McMap. All rights reserved.