如何使用C#从Windows服务运行EXE程序?

62

我该如何使用C#从Windows服务中运行一个EXE程序?

这是我的代码:

System.Diagnostics.Process.Start(@"E:\PROJECT XL\INI SQLLOADER\ConsoleApplication2\ConsoleApplication2\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe");

当我运行这个服务时,应用程序没有启动。
我的代码有什么问题?


1
据我所知,代码本身没有问题,您是否遇到了异常情况? - Quintin Robinson
当您运行此程序时,告诉我们发生了什么? - NT88
这在2019年6月在Visual Studio与.NET 4.7.2应用程序中运行成功:https://code.msdn.microsoft.com/windowsapps/CSCreateProcessAsUserFromSe-b682134e/sourcecode?fileId=50832&pathId=163624599 - Timothy Khouri
@xoops 你有找到解决方案吗?我也遇到了同样的问题。 - Sahir Saiyed
请查看以下文章:https://www.codeproject.com/Tips/1071738/Launching-UI-Application-from-Windows-Service - endo64
9个回答

80
这永远不会起作用,至少在Windows Vista或更高版本下不会。关键问题是你正试图从Windows服务中执行它,而不是标准的Windows应用程序。您展示的代码将在Windows Forms、WPF或Console应用程序中完美运行,但在Windows服务中根本不起作用。

由于Windows服务没有运行在任何特定用户的上下文中,因此无法启动其他应用程序。与常规Windows应用程序不同,服务现在在隔离的会话中运行,并且禁止与用户或桌面交互。这使得该应用程序无处可运行。

有关这些相关问题的答案,请参阅以下信息:

你可能已经意识到,最好的解决方案是创建一个标准的Windows应用程序,而不是服务。这些应用程序旨在由特定用户运行,并与该用户的桌面相关联。这样,您可以随时运行其他应用程序,使用您已经展示的代码。
另一个可能的解决方案是,假设您的控制台应用程序不需要界面或任何输出,则指示进程不要创建窗口。这将防止Windows阻止创建您的进程,因为它将不再请求创建控制台窗口。您可以在相关问题的此答案中找到相关代码。

4
根据EXE的名称,我猜测他试图启动一个控制台应用程序,因此不希望与之交互,所以从服务中运行它是没有问题的。 - Gabe
1
@Gabe:控制台应用程序仍然显示一个界面。如果没有桌面,那么该界面应该显示在哪里? - Cody Gray
2
@Gabe:你说得对,我们都在做可能正确也可能不正确的假设。我认为,与其发布通常会被忽略的澄清请求,不如直接发布一个答案。我的水晶球显示这确实是问题所在,就像之前许多其他人一样。无论如何,了解这一点是值得的。至于应用程序不需要显示UI的可能性,您可以考虑在此答案中提出的解决方案。 - Cody Gray
6
“Windows服务无法启动其他应用程序,因为它们没有在任何特定用户的上下文中运行。”这并不正确,每个服务都有一个用户上下文,可以是系统帐户或普通用户。 - Wojtek Turowicz
2
@Turek:我认为你误解了重点。问题在于Windows服务无法显示UI,因为它们不在用户上下文和桌面中运行。由于控制台应用程序试图打开控制台窗口,所以它们不会起作用。但是在Vista及更高版本中,服务绝对不会在普通用户的上下文中运行。 - Cody Gray
显示剩余9条评论

17

我尝试了这篇文章Code Project,对我来说运行得很好。我也使用了这段代码。这篇文章通过截图进行了很好的解释。

我正在为这种情况添加必要的解释。

当您登录时,系统会为您分配一个唯一的Session ID。在Windows Vista中,第一个登录计算机的用户被操作系统分配Session ID 1。下一个登录的用户将被分配Session ID 2,依此类推。您可以从任务管理器的“用户”选项卡中查看分配给每个登录用户的Session ID。 enter image description here

但是,您的Windows服务被带入Session ID 0。该会话与其他会话隔离。这最终会防止Windows服务调用在用户会话下运行的应用程序,例如1或2。

为了从Windows服务调用应用程序,您需要从winlogon.exe中复制控件,该控件作为当前登录的用户,如下面的屏幕截图所示。 enter image description here

重要代码

// obtain the process id of the winlogon process that 
// is running within the currently active session
Process[] processes = Process.GetProcessesByName("winlogon");
foreach (Process p in processes)
{
    if ((uint)p.SessionId == dwSessionId)
    {
        winlogonPid = (uint)p.Id;
    }
}

// obtain a handle to the winlogon process
hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);

// obtain a handle to the access token of the winlogon process
if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
{
    CloseHandle(hProcess);
    return false;
}

// Security attibute structure used in DuplicateTokenEx and   CreateProcessAsUser
// I would prefer to not have to use a security attribute variable and to just 
// simply pass null and inherit (by default) the security attributes
// of the existing token. However, in C# structures are value types and   therefore
// cannot be assigned the null value.
SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
sa.Length = Marshal.SizeOf(sa);

// copy the access token of the winlogon process; 
// the newly created token will be a primary token
if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, 
    (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, 
    (int)TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
    {
      CloseHandle(hProcess);
      CloseHandle(hPToken);
      return false;
    }

 STARTUPINFO si = new STARTUPINFO();
 si.cb = (int)Marshal.SizeOf(si);

// interactive window station parameter; basically this indicates 
// that the process created can display a GUI on the desktop
si.lpDesktop = @"winsta0\default";

// flags that specify the priority and creation method of the process
int dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;

// create a new process in the current User's logon session
 bool result = CreateProcessAsUser(hUserTokenDup,  // client's access token
                            null,             // file to execute
                            applicationName,  // command line
                            ref sa,           // pointer to process    SECURITY_ATTRIBUTES
                            ref sa,           // pointer to thread SECURITY_ATTRIBUTES
                            false,            // handles are not inheritable
                            dwCreationFlags,  // creation flags
                            IntPtr.Zero,      // pointer to new environment block 
                            null,             // name of current directory 
                            ref si,           // pointer to STARTUPINFO structure
                            out procInfo      // receives information about new process
                            );

对我来说完美地工作了。我能够在通过Windows服务请求扫描时显示扫描仪的错误。谢谢。 - shahar eldad
if so do +1 for the solution. - Pranesh Janarthanan
1
@stambikk 我在Windows 7下使用过它 - Pranesh Janarthanan
1
@stambikk 我正在Windows 10上使用它。 - Antao Almada
1
它在我的本地机器(系统用户)上运行正常,但在客户机器上与活动目录用户不起作用。 - Dr. Rajesh Rolen
显示剩余4条评论

16

您可以使用Windows任务计划程序来实现此目的,有很多类库可以帮助您,例如TaskScheduler

例如,考虑我们想要安排一个任务,在五秒钟后执行一次:

using (var ts = new TaskService())
        {

            var t = ts.Execute("notepad.exe")
                .Once()
                .Starting(DateTime.Now.AddSeconds(5))
                .AsTask("myTask");

        }

notepad.exe将会在五秒钟后被执行。

如需详细信息和更多资讯,请访问wiki

如果您知道需要哪个程序集中的类及方法,您可以像下面这样自行调用:

        Assembly assembly = Assembly.LoadFrom("yourApp.exe");
        Type[] types = assembly.GetTypes();
        foreach (Type t in types)
        {
            if (t.Name == "YourClass")
            {
                MethodInfo method = t.GetMethod("YourMethod",
                BindingFlags.Public | BindingFlags.Instance);
                if (method != null)
                {
                    ParameterInfo[] parameters = method.GetParameters();
                    object classInstance = Activator.CreateInstance(t, null);

                    var result = method.Invoke(classInstance, parameters.Length == 0 ? null : parameters);
                    
                    break;
                }
            }
                         
        }
    

4
我不确定为什么这个答案没有得到更多的关注。Pranesh 的答案建议在 System 账户下运行 Win 服务,但这个选项在大多数企业环境中是不可用的。Rahmat 的答案(仅使用任务计划程序的部分)非常简单,甚至有些好笑——而且在大多数情况下都有效。 - ylax
2
它正在系统用户下运行任务/可执行文件,而我们希望在当前登录的用户下运行它,有什么帮助吗? - Dr. Rajesh Rolen
是的,您可以,请参阅创建以不同用户身份运行的任务 - Rahmat Anjirabi
1
如果我们不知道用户凭据怎么办?@RahmatAnjirabi - Bahadır Öz
@BahadırÖz 你可以尝试这种方式 创建以系统账户运行的任务,希望这个技巧能够帮到你。 - Rahmat Anjirabi

8

得票最高的答案并没有错,但与我会发表的相反。我认为这完全可行,可以启动一个exe文件,并且你可以在任何用户的上下文中执行此操作。逻辑上讲,你不能有任何用户界面或要求用户输入......

以下是我的建议:

  1. 创建一个简单的控制台应用程序,在开始时执行您的服务应该执行的操作,无需用户交互。我真的建议不使用Windows服务项目类型,特别是因为您(目前)不能使用.NET Core。
  2. 添加代码以启动您想从服务调用的exe文件

例如,启动plink.exe。 甚至可以监听输出:

var psi = new ProcessStartInfo()
{
    FileName = "./Client/plink.exe", //path to your *.exe
    Arguments = "-telnet -P 23 127.0.0.1 -l myUsername -raw", //arguments
    RedirectStandardError = true,
    RedirectStandardOutput = true,
    RedirectStandardInput = true,
    UseShellExecute = false,
    CreateNoWindow = true //no window, you can't show it anyway
};

var p = Process.Start(psi);
  1. 使用NSSM(Non-Sucking Service Manager)将控制台应用程序注册为服务。NSSM可以通过命令行进行控制,并可以显示UI以配置服务,或者您可以通过命令行进行配置。如果您知道该用户的登录数据,则可以在任何用户的上下文中运行服务。

我选择了默认的LocalSystem帐户,比Local Service更好。没有必要输入特定用户的登录信息,它可以正常工作。甚至没有勾选“允许服务与桌面交互”复选框,如果您需要更高的权限,则可以勾选。

允许服务与桌面交互选项

最后,我只想说答案排名第一的回答与我的回答完全相反,但我们两个都是正确的,这只是问题的解释方式不同而已 :-D。如果你现在说你不能使用Windows服务项目类型-你可以,但我以前遇到过安装不稳定的情况,直到我找到了NSSM。


3
您应该查看此文章“会话0隔离对Windows中的服务和驱动程序的影响”并下载.docx文件,请仔细阅读,这对我非常有帮助。
然而,这是一个对我的情况很好用的类。
    [StructLayout(LayoutKind.Sequential)]
    internal struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public uint dwProcessId;
        public uint dwThreadId;
    }
    
    [StructLayout(LayoutKind.Sequential)]
    internal struct SECURITY_ATTRIBUTES
    {
        public uint nLength;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }


    [StructLayout(LayoutKind.Sequential)]
    public struct STARTUPINFO
    {
        public uint cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;

    }

    internal enum SECURITY_IMPERSONATION_LEVEL
    {
        SecurityAnonymous,
        SecurityIdentification,
        SecurityImpersonation,
        SecurityDelegation
    }

    internal enum TOKEN_TYPE
    {
        TokenPrimary = 1,
        TokenImpersonation
    }

    public static class ProcessAsUser
    {

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


        [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx", SetLastError = true)]
        private static extern bool DuplicateTokenEx(
            IntPtr hExistingToken,
            uint dwDesiredAccess,
            ref SECURITY_ATTRIBUTES lpThreadAttributes,
            Int32 ImpersonationLevel,
            Int32 dwTokenType,
            ref IntPtr phNewToken);


        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool OpenProcessToken(
            IntPtr ProcessHandle,
            UInt32 DesiredAccess,
            ref IntPtr TokenHandle);

        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool CreateEnvironmentBlock(
                ref IntPtr lpEnvironment,
                IntPtr hToken,
                bool bInherit);


        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool DestroyEnvironmentBlock(
                IntPtr lpEnvironment);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(
            IntPtr hObject);

        private const short SW_SHOW = 5;
        private const uint TOKEN_QUERY = 0x0008;
        private const uint TOKEN_DUPLICATE = 0x0002;
        private const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
        private const int GENERIC_ALL_ACCESS = 0x10000000;
        private const int STARTF_USESHOWWINDOW = 0x00000001;
        private const int STARTF_FORCEONFEEDBACK = 0x00000040;
        private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;


        private static bool LaunchProcessAsUser(string cmdLine, IntPtr token, IntPtr envBlock)
        {
            bool result = false;


            PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
            SECURITY_ATTRIBUTES saProcess = new SECURITY_ATTRIBUTES();
            SECURITY_ATTRIBUTES saThread = new SECURITY_ATTRIBUTES();
            saProcess.nLength = (uint)Marshal.SizeOf(saProcess);
            saThread.nLength = (uint)Marshal.SizeOf(saThread);

            STARTUPINFO si = new STARTUPINFO();
            si.cb = (uint)Marshal.SizeOf(si);


            //if this member is NULL, the new process inherits the desktop
            //and window station of its parent process. If this member is
            //an empty string, the process does not inherit the desktop and
            //window station of its parent process; instead, the system
            //determines if a new desktop and window station need to be created.
            //If the impersonated user already has a desktop, the system uses the
            //existing desktop.

            si.lpDesktop = @"WinSta0\Default"; //Modify as needed
            si.dwFlags = STARTF_USESHOWWINDOW | STARTF_FORCEONFEEDBACK;
            si.wShowWindow = SW_SHOW;
            //Set other si properties as required.

            result = CreateProcessAsUser(
                token,
                null,
                cmdLine,
                ref saProcess,
                ref saThread,
                false,
                CREATE_UNICODE_ENVIRONMENT,
                envBlock,
                null,
                ref si,
                out pi);


            if (result == false)
            {
                int error = Marshal.GetLastWin32Error();
                string message = String.Format("CreateProcessAsUser Error: {0}", error);
                FilesUtilities.WriteLog(message,FilesUtilities.ErrorType.Info);

            }

            return result;
        }


        private static IntPtr GetPrimaryToken(int processId)
        {
            IntPtr token = IntPtr.Zero;
            IntPtr primaryToken = IntPtr.Zero;
            bool retVal = false;
            Process p = null;

            try
            {
                p = Process.GetProcessById(processId);
            }

            catch (ArgumentException)
            {

                string details = String.Format("ProcessID {0} Not Available", processId);
                FilesUtilities.WriteLog(details, FilesUtilities.ErrorType.Info);
                throw;
            }


            //Gets impersonation token
            retVal = OpenProcessToken(p.Handle, TOKEN_DUPLICATE, ref token);
            if (retVal == true)
            {

                SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
                sa.nLength = (uint)Marshal.SizeOf(sa);

                //Convert the impersonation token into Primary token
                retVal = DuplicateTokenEx(
                    token,
                    TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY,
                    ref sa,
                    (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
                    (int)TOKEN_TYPE.TokenPrimary,
                    ref primaryToken);

                //Close the Token that was previously opened.
                CloseHandle(token);
                if (retVal == false)
                {
                    string message = String.Format("DuplicateTokenEx Error: {0}", Marshal.GetLastWin32Error());
                    FilesUtilities.WriteLog(message, FilesUtilities.ErrorType.Info);

                }

            }

            else
            {

                string message = String.Format("OpenProcessToken Error: {0}", Marshal.GetLastWin32Error());
                FilesUtilities.WriteLog(message, FilesUtilities.ErrorType.Info);

            }

            //We'll Close this token after it is used.
            return primaryToken;

        }

        private static IntPtr GetEnvironmentBlock(IntPtr token)
        {

            IntPtr envBlock = IntPtr.Zero;
            bool retVal = CreateEnvironmentBlock(ref envBlock, token, false);
            if (retVal == false)
            {

                //Environment Block, things like common paths to My Documents etc.
                //Will not be created if "false"
                //It should not adversley affect CreateProcessAsUser.

                string message = String.Format("CreateEnvironmentBlock Error: {0}", Marshal.GetLastWin32Error());
                FilesUtilities.WriteLog(message, FilesUtilities.ErrorType.Info);

            }
            return envBlock;
        }

        public static bool Launch(string appCmdLine /*,int processId*/)
        {

            bool ret = false;

            //Either specify the processID explicitly
            //Or try to get it from a process owned by the user.
            //In this case assuming there is only one explorer.exe

            Process[] ps = Process.GetProcessesByName("explorer");
            int processId = -1;//=processId
            if (ps.Length > 0)
            {
                processId = ps[0].Id;
            }

            if (processId > 1)
            {
                IntPtr token = GetPrimaryToken(processId);

                if (token != IntPtr.Zero)
                {

                    IntPtr envBlock = GetEnvironmentBlock(token);
                    ret = LaunchProcessAsUser(appCmdLine, token, envBlock);
                    if (envBlock != IntPtr.Zero)
                        DestroyEnvironmentBlock(envBlock);

                    CloseHandle(token);
                }

            }
            return ret;
        }

    }

执行代码,只需这样调用:

string szCmdline = "AbsolutePathToYourExe\\ExeNameWithoutExtension";
ProcessAsUser.Launch(szCmdline);

2

首先,我们将创建一个在系统帐户下运行的Windows服务。此服务将负责在当前活动用户会话中生成一个交互式进程。这个新创建的进程将显示UI并以完全管理员权限运行。当第一个用户登录计算机时,此服务将启动并在Session0中运行;但是,此服务生成的进程将在当前已登录用户的桌面上运行。我们将称此服务为LoaderService。
接下来,winlogon.exe进程负责管理用户登录和注销过程。我们知道每个登录计算机的用户都具有唯一的会话ID和相应的与其会话关联的winlogon.exe进程。现在,我们上面提到,LoaderService在系统帐户下运行。我们还确认计算机上的每个winlogon.exe进程都在系统帐户下运行。因为系统帐户是LoaderService和winlogon.exe进程的所有者,所以我们的LoaderService可以复制winlogon.exe进程的访问令牌(和会话ID),然后调用Win32 API函数CreateProcessAsUser,在已登录用户的当前活动会话中启动进程。由于复制的winlogon.exe进程访问令牌中的会话ID大于0,因此我们可以使用该令牌启动交互式进程。
试试这个。 在32位和64位架构中颠覆Vista UAC

2
你可以在Windows XP中很好地从Windows服务中执行.exe文件。我过去自己做过。
你需要确保已经在Windows服务属性中勾选了“允许与桌面交互”的选项。如果没有这样做,它将无法执行。
我需要在Windows 7或Vista中进行检查,因为这些版本需要额外的安全权限,所以可能会出现错误,但我相信它可以直接或间接地实现。对于XP,我很确定,因为我自己做过。

每当在Win 7上打开一个exe文件时,它会提示。 - Romil Kumar Jain

0

我认为您正在将.exe文件复制到不同的位置。这可能是问题所在。当您复制exe时,未复制其依赖项。

因此,您可以将所有依赖的dll放入GAC中,以便任何.net exe都可以访问它

否则,请勿将exe复制到新位置。只需创建一个环境变量并在c#中调用exe。由于路径在环境变量中定义,因此c#程序可以访问exe。

更新:

以前我在我的c#.net 3.5项目中遇到了类似的问题,我试图从c#.net代码运行一个.exe文件,而那个exe实际上是另一个项目exe(我添加了一些支持dll来支持我的功能),我在我的exe应用程序中使用了那些dll方法。最后,我通过将该应用程序创建为同一解决方案的单独项目,并将该项目输出添加到我的部署项目中来解决了这个问题。根据这种情况,我回答说,如果这不是他想要的,那么我非常抱歉。


2
你是怎么得出EXE文件被复制到了不同位置的印象的? - Cody Gray

-17

System.Diagnostics.Process.Start("Exe 名称");


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