Add new environment variable to a process launched by ProcessStartInfo.LoadUserProfile - c#

I'm logged on my pc with the user 'podosta'
This is working fine
ProcessStartInfo p = new ProcessStartInfo("c:\myapp.exe");
p.UserName = "myuser";
p.Domain = "mydomain";
p.Password = SecureString;
p.UseShellExecute = false;
p.LoadUserProfile = true;
Process.Start(p);
Environment variables of the process are the environment variable of the user 'myuser'
ex: %USERPROFILE% points to the profile of 'myuser'
This is not working
ProcessStartInfo p = new ProcessStartInfo("c:\myapp.exe");
p.UserName = "myuser";
p.Domain = "mydomain";
p.Password = SecureString;
p.UseShellExecute = false;
p.LoadUserProfile = true;
p.EnvironmentVariables.Add("MY_NEW_VARIABLE", "SOME_TEXT");
Process.Start(p);
Environment variables of the process are the environment variable of me 'podosta'
ex: %USERPROFILE% points to my profile 'podosta'
By the way, %MY_NEW_VARIABLE% is created
Why this behavior ?
I need to start an application with a RunAs, having the environment of the runned user and add some additional environment variable to the process.
Thanks

The behaviour you're seeing is basically document on MSDN for the CreateProcessWithLogon Win32 function which works in the background when starting a process for a different user (using System.Diagnostics.Process):
A pointer to an environment block for the new process. If this
parameter is NULL, the new process uses an environment created from
the profile of the user specified by lpUsername.
So as soon as you specify a single one environment variable for the new process, you need to specify them all.
The problem now is the ProcessStartInfo.EnvironmentVariables property. As soon as you access it the first time (for a given instance of ProcessStartInfo), it will be automatically populated with the complete environment for the current user (ref).
Take this line, which runs in the context of user "podosta":
p.EnvironmentVariables.Add("MY_NEW_VARIABLE", "SOME_TEXT");
It does two things:
fill the p.EnvironmentVariables with the environment of "podosta" (including "USERPROFILE=...podosta").
Add the new variable "MY_NEW_VARIABLE=SOME_TEXT" to that.
This combined set is then passed to the process launched as "myuser" and thus it gets the "wrong" (better: unexpected) value for "USERPROFILE".
If you want to make sure that you pass a NULL environment block to the underlying Win32 function, to get the behavior described in the quote above, you cannot ever "touch" the EnvironmentVariables property - not even iterate and dump it for logging purposes.
It is also not sufficient to do EnvironmentVariables.Clear(). That will not set the internal field to null and the CreateProcessWithLogon function is just passed an empty environment block, but not a null one.
If you fail to do so, you get all sorts of funny behaviours, for example the then-executing process will have a value for USERPROFILE that is that of the user that started the process, etc.
You might get around that stuff by calling CreateEnvironmentBlock via p/invoke, add your environment variables as needed, then calling CreateProcessAsUser (also via p/invoke), passing that modified environment. That is all not an easy undertaking, especially when you want all the features of the Process-class like output redirection, etc.
It is kind of unfortunate that the ProcessStartInfo.EnvironmentVariables property works as it does.

Related

Windows Environment Variable Expansion: Admin vs Non-Admin

I have a C# application that when run sets some environment variables.
These environment variables need to be set system wide.
I use this code to actually do the set.
public static void SetEnvironmentVariable(string _keyName, string _value, RegistryValueKind _type)
{
using (RegistryKey reg = Registry.LocalMachine.OpenSubKey(#"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", true))
{
if (reg != null)
{
reg.SetValue(_keyName, _value, _type);
}
else
{
string x =
string.Format(
"Could find registry key that hosts Environment Variables: SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment");
App.AppLogger.Error(x);
throw new Exception(x);
}
}
}
This code works, but I notice some strange behavior.
Immediately after running my app and this code, I run cmd as a regular user and take a look a the environment using the "set" command.
It shows no changes.
I then run a cmd prompt as admin and run set. It shows the changes. Not only that, it shows fully expanded variables. Where ALLFOO=Foo, and PATH=C:\Windows\System32;%ALLFOO%;, the set command shows PATH=C:\Windows\System32;Foo;.
I then log off and back on. I then run cmd as a normal user.
I type set and it shows the new environment variables, but not expanded.
It shows PATH=C:\Windows\System32;%ALLFOO%; ( It seems to have no trouble expanding %SYSTEMROOT% for some reason.)
I get that the log-off and back on causes the new Explorer.exe that's launched to get the new env vars for the regular user's cmd.exe, but I don't understand why they are not expanded.
Why would running cmd and set as administrator show fully expanded environment variables and running cmd and set as a normal user would not?
When setting the env vars in the C# program i use the RegistryValueKind.ExpandString enumeration.
Edit:
I am aware of the order of declaration time to expansion time issue with system variables and have edited the question example to reflect this.
Changes to the environment variables are not inmediatly visible. You need to notify the running processes of changes to the registry.
And, you should remember that the environment you see in a process (your cmd windows) is not readed from the registry, it is inherited from its parent process. You can send the notification, but some processes will handle it and some not.
The rest of the problems should be explained by the environment load order:
Some values are retrieved from the registry before processing the environment itself. %systemroot% is defined in
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRoot
System environment is loaded
REG_SZ variables, in alphabetic order, are readed first
REG_EXPAND_SZ, in alphabetic order, are readed. As the REG_SZ values have been readed, they can be expanded. If a REG_EXPAND_SZ variable makes reference to another REG_EXPAND_SZ variable that still has not been expanded (alphabetic order), this reference can not be resolved and is not expanded.
When the user logs in, the user environment is processed.
Same REG_SZ, then REG_EXPAND_SZ variable are processed.
Variables defined at user level overwrite the values of the system environment, except for some variables (ex. PATH) where the values are concatenated.
All that means that
Different users can and probably will see different environments
Same user can see different environments depending on what the parent process is. It is not the same to use Win+R and execute cmd than to use Shift+right click in a folder and select "Open command here". Parent processes are different.

Assign A Process Id C#

I am sending messages to a listener windows service. Within the message is a process id. I want to use the process id to attach it to my windows service in order to access a folder on the server.
I cannot see how to set a process id only get one
int nProcessID = Process.GetCurrentProcess().Id;
You can not assign an ID to a process, it is only set by the operating system. You can however search for one by name:
int yourProcess = Process.GetProcessesByName( "YourListener.exe" )[0].Id;
Enhancing a bit the answer of #BillyDvd, I suggest you getting the name of your process instead of hardcoding it, this way, you can change it in your project structure safely:
var name = Process.GetCurrentProcess().ProcessName; // always fix, like "myapp.rms"
var myProcess = Process.GetProcessesByName(name)[0].Id; // variable, given new by Windows OS on every start of your app: 44580, 36960, 38184, ...

C# - Load existing system environment variables when the current process don't have them loaded

On Windows, I have a C# assembly that is COM visible. It references other assemblies to control an application in the machine. It works fine.
However, under Apache Web Server and using CGI, it doesn't work. After doing some debuging, I found out that the problem is that, while running under Apache's CGI, the environment variables SYSTEMROOT and SYSTEMDRIVE, which aparently are needed by the referenced assemblies, are not loaded.
I can configure Apache to pass those environemtn variables too, but before doing so, I'd really like to know if there's some command I can put on my C# COM visible assembly to make it load environment variables as if it was, let's say, the SYSTEM user or something like that, so it doesn't have to relay on the environment passed by the starting application.
How do you force loading an existent system environment variable in C#, when IT IS NOT SET in the current process (or it was process-deleted by the launching process)?
Thanks in advance for any suggestions!
EDIT 1 - ADDED INFO: Just to make it more clear (as I see in the current answers it's not so clear): Apache intendedly deletes a lot of environment variables for CGI processes. It's not that Apache cannot see them, it can, but it won't pass them to CGI processes.
This should do the trick:
Environment.GetEnvironmentVariable("variable", EnvironmentVariableTarget.Machine);
I did a small test and it is working:
//has the value
string a = Environment.GetEnvironmentVariable("TMP");
Environment.SetEnvironmentVariable("TMP", null);
//does not have has the value
a = Environment.GetEnvironmentVariable("TMP");
//has the value
a = Environment.GetEnvironmentVariable("TMP", EnvironmentVariableTarget.Machine);
SOLUTION: Marco's answer was great and technically answered my question - except that I found out that the environment variables SYSTEMROOT and SYSTEMDRIVE are not really set in the registry where all environment variables are set, so, the chosen answer works for all variables except those two, which I specified in the OP.
SYSTEMROOT is defined on the registry in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRoot, and apparently (after more research), SYSTEMDRIVE is generated as a substring of SYSTEMDRIVE.
So, to get SYSTEMDRIVE and SYSTEMROOT from registry and load them into the environment:
using Microsoft.Win32;
namespace MySpace
{
public class Setup
{
public Setup()
{
SetUpEnvironment();
}
private void SetUpEnvironment()
{
string test_a = Environment.GetEnvironmentVariable("SYSTEMDRIVE", EnvironmentVariableTarget.Process);
string test_b = Environment.GetEnvironmentVariable("SYSTEMROOT", EnvironmentVariableTarget.Process);
if (test_a == null || test_a.Length == 0 || test_b == null || test_b.Length == 0)
{
string RegistryPath = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
string SYSTEMROOT = (string) Registry.GetValue(RegistryPath, "SystemRoot", null);
if (SYSTEMROOT == null)
{
throw new System.ApplicationException("Cannot access registry key " + RegistryPath);
}
string SYSTEMDRIVE = SYSTEMROOT.Substring(0, SYSTEMROOT.IndexOf(':') + 1);
Environment.SetEnvironmentVariable("SYSTEMROOT", SYSTEMROOT, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("SYSTEMDRIVE", SYSTEMDRIVE, EnvironmentVariableTarget.Process);
}
}
}
}
Then you can just call Setup setup = new Setup(); from other classes. And that's it. :-)
Environment.GetEnvironmentVariable
see reference here.
e.g.
Environment.CurrentDirectory = Environment.GetEnvironmentVariable("windir");
DirectoryInfo info = new DirectoryInfo(".");
lock(info)
{
Console.WriteLine("Directory Info: "+info.FullName);
}
Are the variables set as system wide?
If they are not, that is what you need to do, otherwise create user variables for the user the COM is running under.
Thank you. I cannot state with any certainty that this has once and for all driven a stake through the heart of the vampire, but amazingly enough, the error has disappeared (for now). The odd thing is that access to the statement
Environment.GetEnvironmentVariable("variable", EnvironmentVariableTarget.Machine);
is a real oddity in the debugger. It does not show up in Intellisense and does not even appear to fire, which leads me to suspect, which you all knew already, that this is some sort of magic runtime object Environment that has no instantiation in the debugger but also can be benignly jumped over. Oh well.
Oh and I should mention that after you see that error, you will note oddities in your Windows OS, which is worrisome. In particular, you will see, if you try to use the Control Panel /System/Advanced Properties (whatever) that it cannot load the dialog for the environment variables any more, indicating that %windir% has been seriously hosed (compromised) across all applications. Bad bad bad....

gettext(i18n) in process running as root/system process

It's not possible to setlocale of a process running as root, how can I use gettext to obtain localized strings (the locale is not auto detected, nor can I force it to a particular value using setlocale) in such a process. Is there any workaround?
Edit -> Adding the code, I used for testing the issue. I force changed the environment variables - LC_ALL, LANGUAGE, LANG to fr_FR.UTF8. I set the locale to fr_FR.UTF8 explicitly too. All the putenv calls returned 0 and the setlocale call returned "C".
int err = putenv("LC_ALL=fr_FR.UTF8");
err = putenv("LANG=fr_FR.UTF8");
err = putenv("LANGUAGE=fr_FR.UTF8");
char *loc = setlocale(LC_ALL, "fr_FR.UTF8");
bindtextdomain("default", "locale");
textdomain("default");
char *text = gettext("hello");
The same code snippet works if I try it in a user process. The exe of both the processes are in same directory, which contains directory locale\fr\LC_MESSAGES\default.mo
Can we change the locale of a process which is running as system process and not user?

Changing environment variables of the calling process

This one seems trivial but the answer has eluded me for a few days now.
I have a Windows batch file, that calls a C# program to do an extra verification that cannot be done in a batch file. After the verification is complete I need to return a status and a string back to the calling shell.
Now the return value is trivial and my C# console app simply sets a return value (exit code if you will). And I thought the string will also be a piece of cake. I attempted to define a new shell variable using the:
Environment.SetEnvironmentVariable("ERR", "Some text");
This call should (and does) define a shell variable within the current process - that is the very C# process that created the variable. The value is lost as soon as the C# app terminates and the shell that created the C# app knows nothing about the variable. So... A call with no particular use... At all... Unless perhaps if I created a child process from the C3 app, perhaps it would inherit my variables.
The EnvironmentVariableTarget.Machine and EnvironmentVariableTarget.User targets for the SetEnvironmentVariable call don't solve the problem either, as only a newly created process will get these new values from the registry.
So the only working solution I can think of is:
write to stdout
write to a file
encode extra meaning into the return value
The first two are a bit ugly and the last one has its limitations and problems.
Any other ideas (how to set a shell variable in the parent process)? Maybe such shell variable modifications are a security concern (think PATH)...
Thank-you for your time.
I had the same problem as Ryan and the only thing that came to my mind as a work-around was to write a batch in error out to set the variable and to call it from the batch.
ConsoleApplication1.exe:
'put some sensible code here
'put result in variable myResult
Dim myResult As String = Guid.NewGuid().ToString("D").ToUpperInvariant()
Console.WriteLine("Normal output from the consonle app")
Console.Error.WriteLine("#ECHO OFF")
Console.Error.WriteLine("SET zzzResult={0}", myResult)
Test.cmd (the calling batch):
#ECHO OFF
:Jump to folder of batch file
PUSHD %~d0%~p0
:Define a temp file
SET zzzTempFile=%TEMP%\TMP%Random%.CMD
:Call .NET console app
ConsoleApplication1.exe 2>%zzzTempFile%
:Call the generated batch file
CALL %zzzTempFile%
:Clean up temp file
DEL %zzzTempFile%
:Clean up variable
SET zzzTempFile=
:Do something with the result
ECHO Yeah, we finally got it!
ECHO:
ECHO The value is "%zzzResult%".
ECHO:
:Clean up result variable
SET zzzResult=
:Go back to original folder
POPD
That should do the trick. And yes, I do know this is an old post and Ryan is solving other issues by now, but there might be still somebody else out there having the same problem...
What you are asking is to be able to arbitrarily write to the memory space of a running process. For good reason, this is not possible without SeDebugPrivilege.
Any of the three solutions you list will work. Stdout is the standard way to communicate with a batch script.
By the way, you're writing a Windows batch file. I'm pretty sure the ship has already sailed on "a bit ugly".
If you want to put a value of some output into a variable in the batch you can use the following construct:
FOR /F "usebackq tokens=4 delims=\[\] " %i IN (`ver`) DO set VERSION=%i
ECHO %VERSION%
Output on my OS:
6.1.7601
'usebackq' means we are using back quotes which gives the ability to use a fileset in the command quoted with double quotes. You may not need this. 'tokens' means the index in the resulting string array to select (it can be a range M-N). If you need to skip lines use 'skip=X'). 'delims' are the string separators to use (like string-Split() in .Net).
You will put your console app instead of 'ver' and adapt the delimiters and tokens to match your specific output. If you have more variables to fill you will need to make the if a bit more complex but that should make a good start.
My BAT is a bit rusty, but I think it's possible to retrieve the 'exit' code from processes you've run externally, perhaps via %ERRORLEVEL%. If that's the case, make sure to exit your program via
Environment.Exit(123); // where 123 = error code
You can't add any messages, so you'll have to do that in the .bat file.
If this isn't the case, stdout is probably the best way.
After stumbling on this myself as well recently, I came up with this approach. What I did is run the bat file using the Process class, i.e.
// Spawn your process as you normally would... but also have it dump the environment varaibles
Process process = new Process();
process.StartInfo.FileName = mybatfile.bat;
process.StartInfo.Arguments = #"&&set>>envirodump.txt";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = false;
process.Start();
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
// Read the environment variable lines into a string array
string[] envirolines = File.ReadAllLines("envirodump.txt");
File.Delete("envirodump.txt");
// Now simply set the environment variables in the parent process
foreach(string line in a)
{
string var = line.Split('=')[0];
string val = line.Split('=')[1];
Environment.SetEnvironmentVariable(var, val);
}
This seems to have worked for me. It's not the cleanest approach, but will work in a bind. :)

Categories

Resources