27

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.

Axel Magagnini
  • 855
  • 1
  • 8
  • 19
  • 9
    Why does this sound like **malware** or a **virus**? – SliverNinja - MSFT Jun 21 '12 at 20:43
  • Cant think of anything else other than a process that monitors.. – nawfal Jun 21 '12 at 20:43
  • 4
    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. – Servy Jun 21 '12 at 20:44
  • 2
    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! – James World Jun 21 '12 at 20:45
  • 4
    Perhaps you are asking how to restart an application if it dies: http://stackoverflow.com/questions/779405/how-do-i-restart-my-c-sharp-winform-application – Richard Morgan Jun 21 '12 at 20:46
  • 23
    @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! – Andrew Savinykh Jun 21 '12 at 20:51
  • 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 there – PedroC88 Jun 21 '12 at 21:02
  • 2
    **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. – Axel Magagnini Jun 21 '12 at 21:03
  • @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! – Axel Magagnini Jun 21 '12 at 21:08
  • 2
    @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. – Gabe Jun 21 '12 at 21:17
  • 1
    I actually didn't even think about the possible malicious uses of this, but just edited the question to clarify the intended usage. – Axel Magagnini Jun 21 '12 at 21:23
  • 8
    @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. – Andrew Savinykh Jun 21 '12 at 21:31
  • 2
    @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. – Gabe Jun 21 '12 at 21:54

4 Answers4

18

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!

Axel Magagnini
  • 855
  • 1
  • 8
  • 19
  • 2
    For anyone using this code in the future : it works very well, but the service MUST run as LocalSystem. – Kevin Coulombe May 18 '13 at 17:13
  • What windows version is supported? – Yazan Jaber Dec 05 '13 at 10:57
  • 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. – Axel Magagnini Dec 05 '13 at 18:00
  • Did you also restart the application on a crash/exit? I'm trying to implement it, but seem to be failing. – Lonefish Oct 25 '16 at 09:05
  • 2
    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. – juicebyjustin Apr 25 '19 at 13:22
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.

A_nto2
  • 1,106
  • 7
  • 16
4

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!");
        }
    }
}
oasten
  • 864
  • 1
  • 7
  • 10
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

Oded Ben Dov
  • 9,936
  • 6
  • 38
  • 53
  • 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. – Axel Magagnini Jun 25 '19 at 13:32
  • 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. @oasten answer is definitely similar, this is just a bit more "code complete" – Oded Ben Dov Jun 26 '19 at 08:15