Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
HomeAnnouncementsFree MagazinesWhite PapersSubmit Content
Discussion GroupsASP.NETWindows FormsLanguages.NET FrameworkVisual Studio.NET
Articles.NET FrameworkASP.NETToolsWindows Forms
.NET DirectoryOpen Source ProjectsUser GroupsWeb Resources
Related Topics
Visual Basic 6SQL ServerMS AccessOther DB ProductsMS Server ProductsMore Topics ...

.NET Forum / .NET Framework / Interop / March 2008

Tip: Looking for answers? Try searching our database.

Redirecting sdtin, stdout, stderr from an already running process

Thread view: 
Enable EMail Alerts  Start New Thread
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.


Rate this thread:







Free Magazines

Get 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 ...

Oracle MagazineNetwork ComputingComputer WorldBio-IT WorldeWeekInformation WeekInfosecurity
 
Sign In
Join
My Latest Posts
My Monitored Threads
My Blog
My Photo Gallery
My Profile
My Homepage

Start New Thread
Enable EMail Alerts
Rate this Thread



©2008 Advenet LLC   Privacy Policy - Terms of Use
This website includes both content owned or controlled by Advenet as well as content owned or controlled by third parties.