关于 C# / .Net / IIS Web Service 调用 exe

发布时间 2023-10-12 19:25:12作者: 古兆洋

转自:https://blog.csdn.net/sby5104/article/details/110189048

最近一个面试,面试官说他们现在的架构是通过IIS 部署的Web Service 调用Server 端的Windows Application 也就是exe。

面试拉跨之后自己尝试了一下这种实现方式,在这里记录一下自己遇到的坑,然后留一下查到的解决方案。

(PS:虽然不理解这种架构的使用场景,但也就是闲得无聊试试看。。。。)

简单写好一个Web Service 的demo 之后,Debug run 了一下,发现一切完美,exe 正常执行,页面打开顺利。

发布至IIS 后发现,访问Web Service 的Method,提示只允许local 调用,应该是Web.config 没有配置HttpGet,添加后解决。

再次尝试,发现Web Service 正常运行,正在等待Process 结束(Method 最后代码为process.WaitForExit()), 但没有exe 的页面弹出。

开始以为是防火墙或代码问题,检查之后发现不是。

打开任务管理器后发现,exe 的process 正常执行,但没有页面。

百度之后发现好多前辈都出过类似的问题,挨个方法尝试。

包括但不限于:

1. 修改IIS 权限

2. 修改IIS machine.config

3. IIS Admin Service, allow service to interact with desktop

以上方法均已失败告终。

转战至Google,刚开始发现的解决方案和百度的一毛一样,直到发现一个回复,回复中说:

Zoltán Zörgő 5-Nov-13 5:07am

As IIS is a service it is not running under a separate session - different from the one you are logged in, and under a different user account! So you can't see the application, even if it is really started. Still, there are only few situations when starting a process on server side from an asp.net application is wise - use this approach only when the application you start is command line application, and never requires user interaction to terminate (even with error condition).

大体上的意思是说:

Windows Service 和Login 桌面是在两个Session 中(此处只Process 的Session ID),而IIS 是一个Windows Service,因此通过IIS 起的Process 无法展示到当前User Login 的桌面中。

按照这个思路往下查询,发现是自Windows Vista 起,巨硬修改了Desktop 的显示,即Login,Windows Service 及UAC 分别处于三个桌面中,Process的SessionID 不同,无法直接相互切换。

在自己的IIS 服务器上打开任务管理器,在Colunms 中选中SessionID 发现,w3wp 的SessionID 为1,而winlogon Process 的SessionID 为0,符合发现的结果。

那么为什么本机Debug 好用呢?

想了一下,可能是Debug 时使用的时IIS Express, 这是个进程,有VS 启动,和winlogon Process 在同一个桌面的Session 中,因此可以成功调用exe 并展示页面。

那,是否有解决方案呢?

身边的C++ 同事说,可以通过获取SessionID,将执行exe 的process 挂到Winlogon 的Session 中解决。

按照同事的思路,Google 到一篇文章,成功解决了个问题,文章的链接为:

How to make windows service interactivity with desktop application

文章中关于这个问题的描述和解决方案都给出了明确的说明,在此就不多做解释了。

为了方便网络可能不那么好的小伙伴,单纯的把代码贴在下面,当然更推荐去阅读一下具体的内容~

代码中需要注意.Net Framework 的版本,我开始使用4.0,发现4.0 没有WindowsIdentity.RunImpersonated,改到4.8 后解决。

其次是

1                  // retrieve the primary access token for the user associated with the specified session Id.
2                 if (!WTSQueryUserToken(curSessionid, out dupedToken))
3                 {
4                     throw new Win32Exception(Marshal.GetLastWin32Error());
5                 }

在Debug 运行时,由于权限及安全性相关问题,无法直接运行。

可以考虑在Debug 时注释掉,或添加Release Tag 以继续Debug。

  1     // Ref: https://stackoverflow.com/questions/19776716/c-sharp-windows-service-creates-process-but-doesnt-executes-it
  2     [SuppressUnmanagedCodeSecurity]
  3     public class ProcessHandler
  4     {
  5         public const int GENERIC_ALL_ACCESS = 0x10000000;
  6         //public const int CREATE_NO_WINDOW = 0x08000000;
  7         public const int STARTF_USESHOWWINDOW = 0x00000001;
  8  
  9         public const int SE_PRIVILEGE_ENABLED = 0x00000002;
 10         public const string SE_INCREASE_QUOTA_NAME = "SeIncreaseQuotaPrivilege";
 11         internal const string SE_TCB_NAME = "SeTcbPrivilege";
 12  
 13  
 14         enum CreateProcessFlags
 15         {
 16             CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
 17             CREATE_DEFAULT_ERROR_MODE = 0x04000000,
 18             CREATE_NEW_CONSOLE = 0x00000010,
 19             CREATE_NEW_PROCESS_GROUP = 0x00000200,
 20             CREATE_NO_WINDOW = 0x08000000,
 21             CREATE_PROTECTED_PROCESS = 0x00040000,
 22             CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
 23             CREATE_SEPARATE_WOW_VDM = 0x00000800,
 24             CREATE_SHARED_WOW_VDM = 0x00001000,
 25             CREATE_SUSPENDED = 0x00000004,
 26             CREATE_UNICODE_ENVIRONMENT = 0x00000400,
 27             DEBUG_ONLY_THIS_PROCESS = 0x00000002,
 28             DEBUG_PROCESS = 0x00000001,
 29             DETACHED_PROCESS = 0x00000008,
 30             EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
 31             INHERIT_PARENT_AFFINITY = 0x00010000
 32         }
 33  
 34  
 35         enum TOKEN_INFORMATION_CLASS
 36         {
 37  
 38             TokenUser = 1,
 39             TokenGroups,
 40             TokenPrivileges,
 41             TokenOwner,
 42             TokenPrimaryGroup,
 43             TokenDefaultDacl,
 44             TokenSource,
 45             TokenType,
 46             TokenImpersonationLevel,
 47             TokenStatistics,
 48             TokenRestrictedSids,
 49             TokenSessionId,
 50             TokenGroupsAndPrivileges,
 51             TokenSessionReference,
 52             TokenSandBoxInert,
 53             TokenAuditPolicy,
 54             TokenOrigin,
 55             TokenElevationType,
 56             TokenLinkedToken,
 57             TokenElevation,
 58             TokenHasRestrictions,
 59             TokenAccessInformation,
 60             TokenVirtualizationAllowed,
 61             TokenVirtualizationEnabled,
 62             TokenIntegrityLevel,
 63             TokenUIAccess,
 64             TokenMandatoryPolicy,
 65             TokenLogonSid,
 66             MaxTokenInfoClass
 67         }
 68  
 69         public enum SECURITY_IMPERSONATION_LEVEL
 70         {
 71             SecurityAnonymous,
 72             SecurityIdentification,
 73             SecurityImpersonation,
 74             SecurityDelegation
 75         }
 76  
 77         public enum TOKEN_TYPE
 78         {
 79             TokenPrimary = 1,
 80             TokenImpersonation
 81         }
 82  
 83         #region struct
 84         [StructLayout(LayoutKind.Sequential)]
 85         public struct STARTUPINFO
 86         {
 87             public Int32 cb;
 88             public string lpReserved;
 89             public string lpDesktop;
 90             public string lpTitle;
 91             public Int32 dwX;
 92             public Int32 dwY;
 93             public Int32 dwXSize;
 94             public Int32 dwXCountChars;
 95             public Int32 dwYCountChars;
 96             public Int32 dwFillAttribute;
 97             public Int32 dwFlags;
 98             public Int16 wShowWindow;
 99             public Int16 cbReserved2;
100             public IntPtr lpReserved2;
101             public IntPtr hStdInput;
102             public IntPtr hStdOutput;
103             public IntPtr hStdError;
104         }
105  
106         [StructLayout(LayoutKind.Sequential)]
107         public struct PROCESS_INFORMATION
108         {
109             public IntPtr hProcess;
110             public IntPtr hThread;
111             public Int32 dwProcessID;
112             public Int32 dwThreadID;
113         }
114  
115         [StructLayout(LayoutKind.Sequential)]
116         public struct SECURITY_ATTRIBUTES
117         {
118             public Int32 Length;
119             public IntPtr lpSecurityDescriptor;
120             public bool bInheritHandle;
121         }
122  
123         [StructLayout(LayoutKind.Sequential, Pack = 1)]
124         public struct TokPriv1Luid
125         {
126             public int Count;
127             public long Luid;
128             public int Attr;
129         }
130         #endregion
131  
132         #region Win32 API
133         [DllImport("advapi32.dll", SetLastError = true)]
134         internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);
135  
136         [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
137         internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
138  
139         [DllImport("advapi32.dll", EntryPoint = "ImpersonateLoggedOnUser", SetLastError = true,
140          CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
141         public static extern IntPtr ImpersonateLoggedOnUser(IntPtr hToken);
142  
143         [
144            DllImport("kernel32.dll",
145               EntryPoint = "CloseHandle", SetLastError = true,
146               CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)
147         ]
148         public static extern bool CloseHandle(IntPtr handle);
149  
150         [
151            DllImport("advapi32.dll",
152               EntryPoint = "CreateProcessAsUser", SetLastError = true,
153               CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)
154         ]
155         public static extern bool
156            CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine,
157                                ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes,
158                                bool bInheritHandle, Int32 dwCreationFlags, IntPtr lpEnvrionment,
159                                string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo,
160                                ref PROCESS_INFORMATION lpProcessInformation);
161  
162         [
163            DllImport("advapi32.dll",
164               EntryPoint = "DuplicateTokenEx")
165         ]
166         public static extern bool
167            DuplicateTokenEx(IntPtr hExistingToken, Int32 dwDesiredAccess,
168                             ref SECURITY_ATTRIBUTES lpThreadAttributes,
169                             Int32 ImpersonationLevel, Int32 dwTokenType,
170                             ref IntPtr phNewToken);
171         [DllImport("advapi32.dll", SetLastError = true)]
172         static extern bool RevertToSelf();
173  
174         [DllImport("Kernel32.dll", SetLastError = true)]
175         public static extern IntPtr WTSGetActiveConsoleSessionId();
176  
177         [DllImport("advapi32.dll")]
178         public static extern IntPtr SetTokenInformation(IntPtr TokenHandle, IntPtr TokenInformationClass, IntPtr TokenInformation, IntPtr TokenInformationLength);
179  
180  
181         [DllImport("wtsapi32.dll", SetLastError = true)]
182         public static extern bool WTSQueryUserToken(uint sessionId, out IntPtr Token);
183         #endregion
184  
185         private static int GetCurrentUserSessionID()
186         {
187             uint dwSessionId = (uint)WTSGetActiveConsoleSessionId();
188  
189             // gets the Id of the User logged in with WinLogOn
190             Process[] processes = Process.GetProcessesByName("winlogon");
191             foreach (Process p in processes)
192             {
193                 if ((uint)p.SessionId == dwSessionId)
194                 {
195  
196                     // this is the process controlled by the same sessionID
197                     return p.SessionId;
198                 } 
199             }
200  
201             return -1;
202         }
203  
204         /// <summary>
205         /// Main method for Create process used advapi32: CreateProcessAsUser
206         /// </summary>
207         /// <param name="filePath">Execute path, for example: c:\app\myapp.exe</param>
208         /// <param name="args">Arugments passing to execute application</param>
209         /// <returns>Process just been created</returns>
210         public static Process CreateProcessAsUser(string filePath, string args)
211         {
212             
213             var dupedToken = IntPtr.Zero;
214  
215             var pi = new PROCESS_INFORMATION();
216             var sa = new SECURITY_ATTRIBUTES();
217             sa.Length = Marshal.SizeOf(sa);
218  
219             try
220             {
221                 // get current token
222                 var token = WindowsIdentity.GetCurrent().Token;
223  
224                 var si = new STARTUPINFO();
225                 si.cb = Marshal.SizeOf(si);
226                 si.lpDesktop = "";
227                 si.dwFlags = STARTF_USESHOWWINDOW;
228  
229                 var dir = Path.GetDirectoryName(filePath);
230                 var fileName = Path.GetFileName(filePath);
231  
232                 // Create new access token for current token
233                 if (!DuplicateTokenEx(
234                     token,
235                     GENERIC_ALL_ACCESS,
236                     ref sa,
237                     (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
238                     (int)TOKEN_TYPE.TokenPrimary,
239                     ref dupedToken
240                 ))
241                 {
242                     throw new Win32Exception(Marshal.GetLastWin32Error());
243                 }
244                 
245                 // got the session Id from user level
246                 uint curSessionid = (uint)GetCurrentUserSessionID();
247                 
248                 // retrieve the primary access token for the user associated with the specified session Id.
249                 if (!WTSQueryUserToken(curSessionid, out dupedToken))
250                 {
251                     throw new Win32Exception(Marshal.GetLastWin32Error());
252                 }
253  
254                 WindowsIdentity.RunImpersonated(WindowsIdentity.GetCurrent().AccessToken, () => 
255                 {
256  
257                     if (!CreateProcessAsUser(
258                                           dupedToken, // user token
259                                           filePath, // app name or path
260                                           string.Format("\"{0}\" {1}", fileName.Replace("\"", "\"\""), args), // command line
261                                           ref sa, // process attributes
262                                           ref sa, // thread attributes
263                                           false, // do not inherit handles
264                                           (int)CreateProcessFlags.CREATE_NEW_CONSOLE, //flags
265                                           IntPtr.Zero, // environment block
266                                           dir, // current dir
267                                           ref si, // startup info
268                                           ref pi // process info
269                                   ))
270                     {
271                         throw new Win32Exception(Marshal.GetLastWin32Error());
272                     }
273                 });
274               
275                 return Process.GetProcessById(pi.dwProcessID);
276             }
277             finally
278             {
279                 // close all open resource
280                 if (pi.hProcess != IntPtr.Zero)
281                     CloseHandle(pi.hProcess);
282                 if (pi.hThread != IntPtr.Zero)
283                     CloseHandle(pi.hThread);
284                 if (dupedToken != IntPtr.Zero)
285                     CloseHandle(dupedToken);
286             }
287         }
288     }

调用时,使用:

1 Process process = ProcessHandler.CreateProcessAsUser("Absolute path of your exe", "args");
2 //you can wait here, but it will block thread
3 //process.WaitForExit();

用完该方法后,每次IIS 收到一个Request, Server 端均会执行你的exe 并展示页面。

 

 

————————————————
版权声明:本文为CSDN博主「sby5104」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sby5104/article/details/110189048