C#/.NET app doesn't recognize Environment Var Change (PATH) - c#

In my C# app, I am programmatically installing an Oracle client if one is not present, which requires adding a dir to the PATH system environment variable. This all works fine, but it doesn't take effect until the user logs out/in to windows, or reboots. How can I get my app to recognize and use the new PATH var without this step? Even restarting my app would be better than requiring the user to log out/in.
Supposedly, broadcasting this change to other processes should work. Here's what I've tried, with no success:
using System.Runtime.InteropServices;
private const int HWND_BROADCAST = 0xffff;
private const int WM_WININICHANGE = 0x001a, WM_SETTINGCHANGE = WM_WININICHANGE, INI_INTL = 1;
[DllImport("user32.dll")]
private static extern int SendMessageTimeoutA(int hWnd, uint wMsg, uint wParam, string lParam, int fuFlags, int uTimeout, int lpdwResult);
int rtnVal = 0;
SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, "Environment", 2, 5000, rtnVal);
I've been told if you stop and restart the process in question, it should pick up these kinds of changes, but restarting my app doesn't do it. I suppose it could be an Oracle issue, that something about Oracle requires the login to recognize the change, I'm not sure. Thanks in advance.

Does Environment.GetEnvironmentVariable("MYVAR", EnvironmentVariableTarget.Machine) not work?
If my app is running elevated then I can
Environment.SetEnvironmentVariable("MYVAR", "cool", EnvironmentVariableTarget.Machine);
//do some other stuff...
Console.WriteLine(Environment.GetEnvironmentVariable("MYVAR", EnvironmentVariableTarget.Machine));
C:\TestApp>>TestApp.exe
cool
I don't know if this will work for other running processes but it should for your app doing the getting/setting

Your problem is only certain apps listen for that message (such as explorer) so it will not be used by your application at all. As the environment is generally inherited then restarting your app from within itself isn't going to help as it will get your current Environment block. If the user restarts from the start menu it will work (assuming the WM_SETTINGCHANGE has been broadcast).
You are best using Environment.GetEnvironmentVariable to read out the current value from the registry and merge it back into you current environment. Basically doing Environment.SetEnvironmentVariable("PATH", Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) + ";" + (Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User)));

In our project we setup Oracle Instant Client with use of "install.bat" from Instant Client archive. For example:
install.bat odp.net1x %1 name

Related

Change wallpaper with service

I'm trying to use a service installed to run as a specific user (me in this case) to change the wallpaper.
Here is my Wallpaper class which has a SetWallpaper function:
public sealed class Wallpaper
{
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError=true)]
private static extern Int32 SystemParametersInfo(
UInt32 action, UInt32 uParam, String vParam, UInt32 winIni);
private static readonly UInt32 SPI_SETDESKWALLPAPER = 0x14;
private static readonly UInt32 SPIF_UPDATEINIFILE = 0x01;
private static readonly UInt32 SPIF_SENDWININICHANGE = 0x02;
public static void SetWallpaper(String path)
{
System.IO.Stream s = new System.Net.WebClient().OpenRead(path.ToString());
System.Drawing.Image img = System.Drawing.Image.FromStream(s);
string tempPath = Path.Combine(Path.GetTempPath(), "wallpaper.bmp");
ImgurWallpaperSetter.ImgurWallpaperSetter.log(tempPath);
img.Save(tempPath, System.Drawing.Imaging.ImageFormat.Bmp);
SystemParametersInfo(SPI_SETDESKWALLPAPER, 1, tempPath,
SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE);
int error = Marshal.GetLastWin32Error();
ImgurWallpaperSetter.ImgurWallpaperSetter.log("Last error: " + error);
}
}
It works perfectly when I run SetWallpaper from a unit test, but it doesn't work at all when I install the service and start it.
Here is the service start code:
protected override void OnStart(string[] args) {
//WallpaperScheduler.ScheduleWallpaperFetch(DateTime.Now.Hour, DateTime.Now.Minute+1);
//Debugger.Launch();
Uri imageUrl = WallpaperRetriever.mostPopularImgurWallpaper();
log(imageUrl.AbsoluteUri);
Wallpaper.SetWallpaper(imageUrl.AbsoluteUri);
}
I've confirmed that it's downloading the image into my temp directory correctly, but it's not setting the wallpaper. It doesn't error out or log anything to the event logs.
Here's my service installed in the local service viewer:
Running it does nothing.
A similar thread I've read
Edit:
Added this code to run on my serviceInstaller_Committed event which should allow the service to interact with the desktop, but I see a huge delay between the service run and the actual switching of the wallpaper:
ConnectionOptions coOptions = new ConnectionOptions();
coOptions.Impersonation = ImpersonationLevel.Impersonate;
ManagementScope mgmtScope = new ManagementScope(#"root\CIMV2", coOptions);
mgmtScope.Connect();
ManagementObject wmiService;
wmiService = new ManagementObject(
"Win32_Service.Name='" + serviceInstaller1.ServiceName + "'"
);
ManagementBaseObject InParam = wmiService.GetMethodParameters("Change");
InParam["DesktopInteract"] = true;
ManagementBaseObject OutParam = wmiService.InvokeMethod("Change", InParam, null);
Edit2:
I've updated my service to log to the system events GetLastError(). Now I am seeing that the service is throwing error 1459 ("This operation requires an interactive window station."). However, this doesn't explain why my wallpaper does eventually switch (I think usually after waking from sleep). Updated Wallpaper class above as well.
Edit3
I've confirmed that after sleeping, the new wallpaper is set. Can anyone explain why this is? Could it be that I need to restart for the Interactive Desktop ability to be set?
Edit4
What I'm doing is feeling pretty hacky. Would it be better if I had the service do nothing but download wallpapers and potentially had another non-service application for changing the wallpaper if new wallpapers have been downloaded and the user is logged in?
Are you aware of Session 0 Isolation? It means that your service is running in a desktop that no user will ever log into, and that restricted environment may very well be affecting your program's behavior.
You say that the code "doesn't error out or log anything to the event logs", but, based on what you have shown, you need to improve the error checking to catch more subtle problems. For example, SystemParametersInfo() returns FALSE when it fails (and a subsequent call to GetLastError() may be very informative!) but your code doesn't check for that result. You can not rely on explicit exceptions alone.
Add this class: http://pastebin.com/ERsnqMEy
Use it like this: http://pastebin.com/RYvvT7bH
Works wonders in using WMI impersonating the logged in user from a windows system service. Best of luck.
In Services, go to Properties, and check "Use Local Account" and something like "Allow Using of Desktop". I'm not sure about names, because my Windows is in a different language, but you should be able to find it.

How to set system environment variable in C#?

I'm trying to set a system environment variable in my application, but get an SecurityException. I tested everything I found in google - without success.
Here is my code (note, that I'm administrator of my pc and run VS2012 as admin):
Attempt 1
new EnvironmentPermission(EnvironmentPermissionAccess.Write, "TEST1").Demand();
Environment.SetEnvironmentVariable("TEST1", "MyTest", EnvironmentVariableTarget.Machine);
Attempt 2
new EnvironmentPermission(EnvironmentPermissionAccess.Write, "TEST1").Demand();
using (var envKey = Registry.LocalMachine.OpenSubKey(#"SYSTEM\CurrentControlSet\Control\Session Manager\Environment", true))
{
Contract.Assert(envKey != null, #"HKLM\System\CurrentControlSet\Control\Session Manager\Environment is missing!");
envKey.SetValue("TEST1", "TestValue");
}
Attempt 3
Also I tried to fit out my app with administrator priviliges.
Do you have any other suggestions?
The documentation tells you how to do this.
Calling SetEnvironmentVariable has no effect on the system environment variables. To programmatically add or modify system environment variables, add them to the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment registry key, then broadcast a WM_SETTINGCHANGE message with lParam set to the string "Environment". This allows applications, such as the shell, to pick up your updates.
So, you need to write to the registry setting that you are already attempting to write to. And then broadcast a WM_SETTINGCHANGE message as detailed above. You will need to be running with elevated rights in order for this to succeed.
Some example code:
using Microsoft.Win32;
using System;
using System.Diagnostics.Contracts;
using System.Runtime.InteropServices;
namespace ConsoleApplication1
{
class Program
{
const int HWND_BROADCAST = 0xffff;
const uint WM_SETTINGCHANGE = 0x001a;
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern bool SendNotifyMessage(IntPtr hWnd, uint Msg,
UIntPtr wParam, string lParam);
static void Main(string[] args)
{
using (var envKey = Registry.LocalMachine.OpenSubKey(
#"SYSTEM\CurrentControlSet\Control\Session Manager\Environment",
true))
{
Contract.Assert(envKey != null, #"registry key is missing!");
envKey.SetValue("TEST1", "TestValue");
SendNotifyMessage((IntPtr)HWND_BROADCAST, WM_SETTINGCHANGE,
(UIntPtr)0, "Environment");
}
}
}
}
However, whilst this code does work, the .net framework provides functionality to perform the same task much more simply.
Environment.SetEnvironmentVariable("TEST1", "TestValue",
EnvironmentVariableTarget.Machine);
The documentation for the three argument Environment.SetEnvironmentVariable overload says:
If target is EnvironmentVariableTarget.Machine, the environment variable is stored in the HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\Environment key of the local computer's registry. It is also copied to all instances of File Explorer. The environment variable is then inherited by any new processes that are launched from File Explorer.
If target is User or Machine, other applications are notified of the set operation by a Windows WM_SETTINGCHANGE message.

"No Disk" error using GDAL from C#/.NET

I am using Tamas Szekeres builds of GDAL including the C# bindings in a desktop GIS application using C# and .net 4.0
I am including the entire GDAL distribution in a sub-directory of my executable with the following folder structure:
\Plugins\GDAL
\Plugins\GDAL\gdal
\Plugins\GDAL\gdal-data
\Plugins\GDAL\proj
We are using EPSG:4326, and the software is built using 32-bit target since the GDAL C# API is using p/invoke to the 32-bit libraries (could try 64 bit since Tamas provides these, haven't gotten around to it yet).
When I run my application I get the following error
This error typically happens when software tries to access a device that is no longer attached, such as a removable drive. It is not possible to "catch" this exception because it pops up a system dialog.
After dismissing the dialog using any of the buttons, the software continues to execute as designed.
The error occurs the first time I call the following method
OSGeo.OSR.CoordinateTransformation.TransformPoint(double[] inout);
The strange stuff:
The error occurs on one, and only one computer (so far)
I've run this software in several other computers both 32 and 64 bit without problems
The error does not ocurr on the first run after compiling the GDAL shim library I am using, it only occurrs on each subsequent run
it happens regardless of release, or debug builds
it happens regardless of whether the debugger is attached or not
it happens regardless of whether I turn on or off Gdal.UseExceptions or Osr.UseExceptions();
disabling removable drives causes the bug to disappear. This is not what I consider a real solution as I will not be able to ask a customer to do this.
I have tried the following:
catching the error
changing GDAL directories and environment settings
changing computers and operating systems: this worked
used SysInternals ProcMon to trace what files are being opened with no luck, they all appear to be files that exist
I re-built the computer in question when the hard drive failed, to no avail.
"cleaning" the registry using CCleaner
files in GDAL Directory are unchanged on execution
Assumptions
Error is happening in unmanaged code
During GDAL initialization, some path is referring to a drive on the computer that is no longer attached.
I am also working on the assumption this is limited to a computer configuration error
Configuration
Windows 7 Pro
Intel Core i7 920 # 2,67GHz
12.0 GB RAM
64-bit OS
Drive C: 120 GB SSD with OS, development (Visual Studio 10), etc
Drive D: 1 TB WD 10,000k with data, not being accessed for data.
The Question
I either need a direction to trap the error, or a tool or technique that will allow me to figure out what is causing it. I don't want to release the software with the possibility that some systems will have this behaviour.
I have no experience with this library, but perhaps some fresh eyes might give you a brainwave...
Firstly, WELL WRITTEN QUESTION! Obviously this problem really has you stumped...
Your note about the error not occurring after a rebuild screams out: Does this library generate some kind of state file, in its binary directory, after it runs?
If so, it is possible that it is saving incorrect path information into that 'configuration' file, in a misguided attempt to accelerate its next start-up.
Perhaps scan this directory for changes between a 'fresh build' and 'first run'?
At very least you might find a file you can clean up on shut-down to avoid this alert...
HTH
Maybe you can try this:
Run diskmgmt.msc
Change the driveletter for Disk 2 (right click) if my assumption that Disk 2 is a Removable Disk is true
Run your application
If this removes the error, something in the application is referring to the old driveletter
It could be in the p/invoked libs
Maybe see: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=46501 It talks about gcc somehow compiling a driveletter into a binary
+1 Great question, but It is not possible to "catch"
Its one of these awful solutions that will turn up on DailyWTF in 5 years. But for now it is stored here http://www.pinvoke.net/default.aspx/user32.senddlgitemmessage
using Microsoft.VisualBasic; //this reference is for the Constants.vbNo;
public partial class Form1 : Form
{
[DllImport("user32.dll")]
static extern IntPtr SendDlgItemMessage(IntPtr hDlg, int nIDDlgItem, uint Msg, UIntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetActiveWindow(IntPtr hWnd);
// For Windows Mobile, replace user32.dll with coredll.dll
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
// Find window by Caption only. Note you must pass IntPtr.Zero as the first parameter.
[DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)]
static extern IntPtr FindWindowByCaption(IntPtr ZeroOnly, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetDlgItemText(IntPtr hDlg, int nIDDlgItem,[Out] StringBuilder lpString, int nMaxCount);
public void ClickSaveBoxNoButton()
{
//In this example, we've opened a Notepad instance, entered some text, and clicked the 'X' to close Notepad.
//Of course we received the 'Do you want to save...' message, and we left it sitting there. Now on to the code...
//
//Note: this example also uses API calls to FindWindow, GetDlgItemText, and SetActiveWindow.
// You'll have to find those separately.
//Find the dialog box (no need to find a "parent" first)
//classname is #32770 (dialog box), dialog box title is Notepad
IntPtr theDialogBoxHandle; // = null;
string theDialogBoxClassName = "#32770";
string theDialogBoxTitle = "Notepad";
int theDialogItemId = Convert.ToInt32("0xFFFF", 16);
StringBuilder theDialogTextHolder = new StringBuilder(1000);
//hardcoding capacity - represents maximum text length
string theDialogText = string.Empty;
string textToLookFor = "Do you want to save changes to Untitled?";
bool isChangeMessage = false;
IntPtr theNoButtonHandle; // = null;
int theNoButtonItemId = (int)Constants.vbNo;
//actual Item ID = 7
uint theClickMessage = Convert.ToUInt32("0x00F5", 16);
//= BM_CLICK value
uint wParam = 0;
uint lParam = 0;
//Get a dialog box described by the specified info
theDialogBoxHandle = FindWindow(theDialogBoxClassName, theDialogBoxTitle);
//a matching dialog box was found, so continue
if (theDialogBoxHandle != IntPtr.Zero)
{
//then get the text
GetDlgItemText(theDialogBoxHandle, theDialogItemId, theDialogTextHolder, theDialogTextHolder.Capacity);
theDialogText = theDialogTextHolder.ToString();
}
//Make sure it's the right dialog box, based on the text we got.
isChangeMessage = Regex.IsMatch(theDialogText, textToLookFor);
if ((isChangeMessage))
{
//Set the dialog box as the active window
SetActiveWindow(theDialogBoxHandle);
//And, click the No button
SendDlgItemMessage(theDialogBoxHandle, theNoButtonItemId, theClickMessage, (System.UIntPtr)wParam, (System.IntPtr)lParam);
}
}
It turns out there was no way to definitely answer this question.
I ended up "solving" the problem by figuring out that there was some hardware registered on the system that wasn't present. It is still a mystery to me why, after several years, only GDAL managed to provoke this bug.
I will put the inability to catch this exception down to the idiosyncrasies involved with p/invoke and the hardware error thrown at a very low level on the system.
You could add custom error handlers to gdal. This may help:
Link
http://trac.osgeo.org/gdal/ticket/2895

GetVolumeInformation for network drives when admin returns 0

I'm using this code to retrieve the volume serial
[DllImport("Kernel32.dll", SetLastError = true)]
extern static bool GetVolumeInformation(string vol, StringBuilder name, int nameSize, out uint serialNum, out uint maxNameLen, out uint flags, StringBuilder fileSysName, int fileSysNameSize);
public static uint GetVolumeSerial(string strDriveLetter)
{
uint serialNum, maxNameLen, flags;
bool ok = GetVolumeInformation(strDriveLetter, null, 0, out serialNum,out maxNameLen, out flags, null, 0);
return serialNum;
}
It works great, except when I'm running as administrator through the application manifest UAC elevation it always returns 0 for mapped network drives (but it works otherwise)
Maybe the administrator user doesn't see the mapped drive for some reason (you'd think it was the other way around). Is there any way around this or simply invoking that code as the logged in user instead?
I'm on 64-bits Windows 7, but running the application in x86 mode
Mapped network drives are mapped for one user. When you connect as a different user, as far as that other user is concerned, the drive doesn't exist.
If you need several users to access the same network path, use the actual UNC path ("\\server\path"). A mapped network drive is just a convenience for humans.
If you really need several users to access the mapped network drive, you'll need to map it for each user separately. You could, for example, do this in a batch file that runs on logon of each user and calls net use (I'm not quite sure it would help with users such as SYSTEM, though).

Access denied while getting process path

I am trying to get process path by pid but I'm getting Win32Exception (access id denied).
The code looks like this:
string path = Process.GetProcessById(pid).MainModule.FileName
I have tried using OpenProcess with GetModuleFileNameEx but OpenProcess is returning 0. I even tried enabling SeDebugPrivilege according to C# – How to enable SeDebugPrivilege but it didn't help.
The above code works for most of the processes but throws error for SynTPHelper.exe (Synaptics Pointing Device Helper) The application is running under the same username as my code. Both, my application and the process run in 64 bit.
Is it possible to retrieve the path without running my application as an administrator?
Edit
Task Manager is able to 'open file location' even though I'm not running it as an administrator.
Finally I managed to solve it. As it turned out there is new function in Vista and above for getting process path and new process access (PROCESS_QUERY_LIMITED_INFORMATION):
QueryFullProcessImageName
Here is the code that works from non-elevated process:
private static string GetExecutablePathAboveVista(UIntPtr dwProcessId)
{
StringBuilder buffer = new StringBuilder(1024);
IntPtr hprocess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_LIMITED_INFORMATION, false, dwProcessId);
if (hprocess != IntPtr.Zero)
{
try
{
int size = buffer.Capacity;
if (QueryFullProcessImageName(hprocess, 0, buff, out size))
{
return buffer.ToString();
}
}
finally
{
CloseHandle(hprocess);
}
}
return string.Empty;
}
Well, it is certainly not unheard of for services to remove access rights so that even an administrator cannot open the process. A service has enough privileges to do so, DRM components like audiodg.exe readily do so. A mouse pad helper doesn't strike me as something that would require such protection. But what the hey, why would anybody ever need to mess with a mouse pad helper?

Categories

Resources