.NET Forum / .NET Framework / Interop / March 2008
Redirecting sdtin, stdout, stderr from an already running process
|
|
Thread rating:  |
ghandi - 21 Feb 2008 03:02 GMT I am trying to redirect stdin, stdout, stderr of a process I started with the win32 call CreateProcessAsUser, since I couldn't find a way to start a process with .net that used a user name and password and didn't show any kind of window. The only way I can see to do redirect the input and output now is to continue to use the win32 API (maybe a pipe?). Is there a way to do this with .net? Can I get the process input and output into a stream or something?
Jeroen Mostert - 21 Feb 2008 20:02 GMT > I am trying to redirect stdin, stdout, stderr of a process I started > with the win32 call CreateProcessAsUser, since I couldn't find a way > to start a process with .net that used a user name and password and > didn't show any kind of window. This appears to be a design flaw in Process. If you specify user credentials, Process.Start() calls CreateProcessWithLogonW(). Unfortunately, unlike CreateProcessAsUser(), that function does not respect the CREATE_NO_WINDOW flag that is passed when you set .CreateNoWindow to true (which would normally suppress creation of a new window).
> The only way I can see to do redirect the input and output now is to > continue to use the win32 API (maybe a pipe?). Is there a way to do this > with .net? Can I get the process input and output into a stream or > something? You cannot redirect I/O externally after the process has started; only the process itself can do that.
You should pass appropriate handles in the STARTUPINFO[EX] parameter of the CreateProcessAsUser() function, and set the STARTF_USESTDHANDLES flag in the dwFlags member. Read the MSDN entry on STARTUPINFO carefully to know the rules for this.
If you use a managed FileStream for the redirecting, you can use the .SafeFileHandle property to get the necessary OS handle. You could use a pipe for this, but there are no standard managed classes for creating pipes, so you'll have to P/Invoke to CreatePipe as well. You can create a FileStream from the resulting handle.
 Signature J.
ghandi - 21 Feb 2008 23:35 GMT > > I am trying to redirect stdin, stdout, stderr of a process I started > > with the win32 call CreateProcessAsUser, since I couldn't find a way [quoted text clipped - 28 lines] > -- > J. Thanks for the info. I saw the MSDN docs on STARTUPINFO last night and wondered if I could use those handles with some sort of stream object in .net. I'll give that a try and see what happens. Thanks again for your time.
ghandi - 24 Feb 2008 00:50 GMT > > > I am trying to redirect stdin, stdout, stderr of a process I started > > > with the win32 call CreateProcessAsUser, since I couldn't find a way [quoted text clipped - 33 lines] > object in .net. I'll give that a try and see what happens. > Thanks again for your time. I created three FileStreams like this: FileStream stdin = new FileStream("tmpin", FileMode.Create); FileStream stdout = new FileStream("tmpout", FileMode.Create); FileStream stderr = new FileStream("tmperr", FileMode.Create); then tried to pass the file handle created to the STARTUPINFO structure (si) like this: si.hStdInput = stdin.SafeFileHandle.DangerousGetHandle(); si.hStdOutput = stdout.SafeFileHandle.DangerousGetHandle(); si.hStdError = stderr.SafeFileHandle.DangerousGetHandle(); next, I started the process like this: result = CreateProcessAsUser(token, null, fullProcessName, ref saP, ref saP, true, creationFlags, env, null, ref si, out pi); After that I created a StreamWriter and two StreamReaders with the FileStreams I created earlier. I thought this would allow me to read and write to the streams that would now point to stdin, stdout, and stderr of the created process. I thought wrong. I checked the SafeFileHandles before and after the process was created to make sure they were the same, not closed, and not invalid. They seem just fine. Is that what you were suggesting? Thanks.
Jeroen Mostert - 24 Feb 2008 01:22 GMT >>>> I am trying to redirect stdin, stdout, stderr of a process I started >>>> with the win32 call CreateProcessAsUser, since I couldn't find a way [quoted text clipped - 31 lines] > FileStream stdout = new FileStream("tmpout", FileMode.Create); > FileStream stderr = new FileStream("tmperr", FileMode.Create); This will create three *files* named tmpin, tmpout and tmperr, respectively.
> then tried to pass the file handle created to the STARTUPINFO > structure (si) like this: > si.hStdInput = stdin.SafeFileHandle.DangerousGetHandle(); > si.hStdOutput = stdout.SafeFileHandle.DangerousGetHandle(); > si.hStdError = stderr.SafeFileHandle.DangerousGetHandle(); This causes the process to read from "tmpin", write to "tmpout" and dump errors to "tmperr". As in, immediately. It will read 0 bytes from tmpin and that's it, and then it'll write to "tmpout" and "tmperr" if the permissions line up.
> After that I created a StreamWriter and two StreamReaders with the > FileStreams I created earlier. > I thought this would allow me to read and write to the streams that > would now point to stdin, stdout, and stderr of the created process. > I thought wrong. Indeed you did. That said, if the process wrote anything to stdout/stderr, the "tmpout" and "tmperr" files should contain something. If they don't, make sure you've set STARTF_USESTDHANDLES in the "dwFlags" member of STARTUPINFO, and make sure there's no problem with starting the process under different credentials (try a null test with CreateProcess() rather than CreateProcessAsUser() to verify if that's a problem).
> I checked the SafeFileHandles before and after the > process was created to make sure they were the same, not closed, and > not invalid. They seem just fine. > Is that what you were suggesting? No. My suggestion was to use CreatePipe() to create new pipes, then create FileStreams around the handles to these pipes:
[DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, [In] ref SECURITY_ATTRIBUTES lpPipeAttributes, int nSize);
To make it a bit more manageable, let's make a utility class:
internal sealed class Pipe : IDisposable { private FileStream readStream; public FileStream ReadStream { get { return readStream; } }
private FileStream writeStream; public FileStream WriteStream { get { return writeStream; } }
public Pipe(SECURITY_ATTRIBUTES securityAttributes) { SafeFileHandle readHandle, writeHandle; if (!NativeMethods.CreatePipe(out readHandle, out writeHandle, ref securityAttributes, 0)) { throw new Win32Exception(); } readStream = new FileStream(readHandle, FileAccess.Read); writeStream = new FileStream(writeHandle, FileAccess.Write); }
public void Dispose() { if (readStream!= null) readStream.Dispose(); if (writeStream!= null) writeStream.Dispose(); } }
Now, let's make the necessary pipes:
// I'm not writing this out, but you need to pass valid SECURITY_ATTRIBUTES here granting the user access to the pipe, otherwise the pipe handle will not be inheritable and the child process will not be able to access it
using (Pipe stdInPipe = new Pipe(securityAttributes), stdOutPipe = new Pipe(securityAttributes), stdErrPipe = new Pipe(securityAttributes)) { FileStream stdInStream = stdInPipe.WriteStream; FileStream stdOutStream = stdOutPipe.ReadStream; FileStream stdErrStream = stdErrPipe.ReadStream;
si.hStdInput = stdInStream.SafeFileHandle.DangerousGetHandle(); si.hStdOutput = stdOutStream.SafeFileHandle.DangerousGetHandle(); si.hStdError = stdErrStream.SafeFileHandle.DangerousGetHandle();
// Now, start the process
// Write to stdInStream, read from stdOutStream and stdErrStream }
How's that?
 Signature J.
ghandi - 25 Feb 2008 19:00 GMT > >>>> I am trying to redirect stdin, stdout, stderr of a process I started > >>>> with the win32 call CreateProcessAsUser, since I couldn't find a way [quoted text clipped - 128 lines] > -- > J. Thanks for the help there. I'll give this stuff a try today and tomorrow. I really appreciate the help.
ghandi - 01 Mar 2008 20:42 GMT I got a chance to try your advise out. I think I am doing something wrong though. I think it has something to do with the way I am creating the process. Here's my pipe class: sealed class Pipe : IDisposable { [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, [In] ref SECURITY_ATTRIBUTES lpPipeAttributes, int nSize);
private FileStream m_readStream; private FileStream m_writeStream; private ILog m_logg = LogManager.GetLogger(typeof(Pipe));
public FileStream ReadStream { get { return m_readStream; } }
public FileStream WriteStream { get { return m_writeStream; } }
public Pipe(SECURITY_ATTRIBUTES se) { SafeFileHandle readHandle; SafeFileHandle writeHandle; if (!CreatePipe(out readHandle, out writeHandle, ref se, 0)) { int errorNum = Marshal.GetLastWin32Error(); m_logg.Error("****** Creation of pipe failed. Error " + errorNum + " ******"); } m_logg.Info("Successfully created pipe with read handle " + readHandle.DangerousGetHandle() + " and write handle " + writeHandle.DangerousGetHandle()); m_readStream = new FileStream(readHandle, FileAccess.Read); m_writeStream = new FileStream(writeHandle, FileAccess.Write); }
#region IDisposable Members
public void Dispose() { if (m_readStream != null) { m_readStream.Dispose(); } if (m_writeStream != null) { m_writeStream.Dispose(); } }
#endregion }
Here I create the pipe: private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400; private const uint CREATE_NO_WINDOW = 0x08000000; private const uint STARTF_USESTDHANDLES = 0x00000100; SECURITY_ATTRIBUTES pipeOutSa = new SECURITY_ATTRIBUTES(); Pipe stdout; pipeOutSa.nLength = (uint)Marshal.SizeOf(pipeOutSa); pipeOutSa.bInheritHandle = true; stdout = new Pipe(pipeOutSa); FileStream m_outStream = stdout.ReadStream;
Here I start the process: PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); STARTUPINFO si = new STARTUPINFO(); SECURITY_ATTRIBUTES saP = new SECURITY_ATTRIBUTES(); SECURITY_ATTRIBUTES saT = new SECURITY_ATTRIBUTES(); IntPtr env = new IntPtr(0); bool result = false; uint creationFlags = CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW; si.cb = (uint)Marshal.SizeOf(si); saP.nLength = (uint)Marshal.SizeOf(saP); saT.nLength = (uint)Marshal.SizeOf(saT); si.hStdInput = m_inStream.SafeFileHandle.DangerousGetHandle(); si.hStdOutput = m_outStream.SafeFileHandle.DangerousGetHandle(); si.hStdError = m_errStream.SafeFileHandle.DangerousGetHandle(); si.dwFlags = STARTF_USESTDHANDLES; saP.bInheritHandle = true; saT.bInheritHandle = true;
result = CreateProcessAsUser(token, null, fullProcessName, ref saP, ref saP, true, creationFlags, env, null, ref si, out pi);
I used this simple c++ program to test it out: #include <iostream> #include <string>
using namespace std; int main() { string hold = ""; while(true) { cout << "I should be printing to stdout." << endl; std::getline(cin, hold); cout << "This is what you entered." << hold << endl; cerr << "Trying out what should be stderr." << endl; } } After the process is started, the test program uses up all of the CPU. When I try to use stdout.EndOfStream or stdout.Peek(), it just stalls waiting for the test program to return. Am I creating the process wrong? Am I passing the wrong flag or something? I have tried it with just CreateProcess and get the same thing. I have also tried it with different executables (like cmd.exe and powershell.exe) and I get nothing from stdin, stdout, or stderr on those either. Thanks for your time.
Jeroen Mostert - 02 Mar 2008 12:47 GMT > I am trying to redirect stdin, stdout, stderr of a process I started > with the win32 call CreateProcessAsUser, since I couldn't find a way [quoted text clipped - 3 lines] > pipe?). Is there a way to do this with .net? Can I get the process > input and output into a stream or something? Alright, let's restart this one from the top because it's a real hornet's nest. To summarize:
The issue at hand is that we wish to start a process under another user's credentials with redirected I/O, without displaying a new window for that process. Normally, this is accomplished by calling Process.Start() with a ProcessStartInfo structure whose property "CreateNoWindow" is set to true and whose "Redirect*" properties are set to appropriate values. However, setting "CreateNoWindow" has no effect when also setting the "UserName" and "Password" properties. The reason it has no effect is that Process calls CreateProcessWithLogonW() to start the new process, and this function does not support the CREATE_NO_WINDOW flag that "CreateNoWindow" maps to.
One might be tempted to use a combination of LogonUser()/CreateProcessAsUser() instead. This has great problems of its own, however. In order to use CreateProcessAsUser() successfully, the caller must hold the SE_ASSIGNPRIMARYTOKEN_NAME and SE_INCREASE_QUOTA_NAME privileges. By default, on most systems, the only accounts that hold this privilege are the NetworkService and LocalService accounts. Not even administrators have this privilege by default. CreateProcessAsLogonW() is recommended as the successor to this combination, since it does not require additional privileges.
Moreover, using any of the unmanaged CreateProcess*() functions in combination with I/O redirection is cumbersome. The basic approach is to use inheritable handles anonymous pipes, but there are many pitfalls. The MSDN contains a sample for unmanaged code that clearly demonstrates the difficulties involved: http://support.microsoft.com/kb/q190351/
A much simpler approach is to use impersonation, then use Process to start the process regularly:
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool LogonUser(string lpszUserName, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken);
...
const int LOGON32_LOGON_INTERACTIVE = 2; const int LOGON32_PROVIDER_DEFAULT = 0; IntPtr userToken; if (!LogonUser(userName, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out userToken) { throw new Win32Exception(); }
ProcessStartupInfo startupInfo; ... startupInfo.RedirectStandardOutput = true; startupInfo.UseShellExecute = false; startupInfo.CreateNoWindow = true;
Process process; using (WindowsIdentity identity = new WindowsIdentity(userToken)) { using (WindowsImpersonationContext impersonationContext = identity.Impersonate()) { process = Process.Start(startupInfo); } } Console.WriteLine(process.StandardOutput.ReadToEnd());
This, finally, works on my system. Is it of use to you too?
 Signature J.
Jeroen Mostert - 02 Mar 2008 12:59 GMT <snip>
> A much simpler approach is to use impersonation, then use Process to > start the process regularly: <snip>
> This, finally, works on my system. Is it of use to you too? Never mind, of course this doesn't work, and I'm a very sloppy tester.
Really, by this point, I can really only advise two courses of action:
- Give up and accept that there will be a window. - After the process has started, use ugly code to find the window and hide it manually.
Doing it "properly" is way, way more trouble than its worth.
 Signature J.
ghandi - 03 Mar 2008 05:52 GMT > <snip>> A much simpler approach is to use impersonation, then use Process to > > start the process regularly: [quoted text clipped - 13 lines] > -- > J. Thanks again for your time. I'll try a few more things and then try one of the things you mentioned (probably give up and accept that there will be a window). I was also looking at the new namespace System.IO.Pipes. It has an AnonymousPipeServerStream that might be useful.
Jeroen Mostert - 03 Mar 2008 17:31 GMT >> <snip>> A much simpler approach is to use impersonation, then use Process to >>> start the process regularly: [quoted text clipped - 9 lines] >> >> Doing it "properly" is way, way more trouble than its worth. > Thanks again for your time. I'll try a few more things and then try
> one of the things you mentioned (probably give up and accept that > there will be a window). I was also looking at the new namespace > System.IO.Pipes. It has an AnonymousPipeServerStream that might be > useful. If you don't mind the dependency on .NET 3.5 (since that's when they were introduced) then using the native pipe classes is definitely preferable to rolling your own. (I only recently discovered them myself, otherwise I would have recommended them.)
 Signature J.
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|