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.
--
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.
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.
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?
--
J.
Thanks for the help there. I'll give this stuff a try today and
tomorrow. I really appreciate the help.
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.
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?
--
J.
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.
--
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.
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.)
--
J.