Delete a directory where someone has opened a file - c#

I am trying to programmatically delete and replace the contents of an application, "App A", using an "installer" program, which is just a custom WPF .exe app, we'll call "App B". (My question concerns code in "App B".)
GUI Setup (not particularly important)
App B has a GUI where a user can pick computer names to copy App A onto. A file picker is there the admin uses to fill in the source directory path on the local machine by clicking "App A.exe". There are also textboxes for a user name and password, so the admin can enter their credentials for the target file server where App A will be served - the code impersonates the user with these to prevent permission issues. A "Copy" button starts the routine.
Killing App A, File Processes, and Doing File Deletion
The Copy routine starts by killing the "App A.exe" process on all computers in the domain, as well as explorer.exe, in case they had App A's explorer folder open. Obviously this would be done afterhours, but someone may still have left things open and locked their machine before going home. And that's really the base of the problem I'm looking to solve.
Prior to copying over the updated files, we want to delete the entire old directory. In order to delete the directory (and its subdirectories), each file within them has to be deleted. But say they had a file open from App A's folder. The code finds any locking process on any file prior to deleting it (using code from Eric J.'s answer at How do I find out which process is locking a file using .NET? ), it kills that process on whatever computer it is running on. If local, it just uses:
public static void localProcessKill(string processName)
{
foreach (Process p in Process.GetProcessesByName(processName))
{
p.Kill();
}
}
If remote, it uses WMI:
public static void remoteProcessKill(string computerName, string fullUserName, string pword, string processName)
{
var connectoptions = new ConnectionOptions();
connectoptions.Username = fullUserName; // #"YourDomainName\UserName";
connectoptions.Password = pword;
ManagementScope scope = new ManagementScope(#"\\" + computerName + #"\root\cimv2", connectoptions);
// WMI query
var query = new SelectQuery("select * from Win32_process where name = '" + processName + "'");
using (var searcher = new ManagementObjectSearcher(scope, query))
{
foreach (ManagementObject process in searcher.Get())
{
process.InvokeMethod("Terminate", null);
process.Dispose();
}
}
}
Then it can delete the file. All is well.
Directory Deletion Failure
In my code below, it is doing the recursive deletion of the files, and does it fine, up until the Directory.Delete(), where it will say The process cannot access the file '\\\\SERVER\\C$\\APP_A_DIR' because it is being used by another process, because I am attempting to delete the directory while I had a file still open from it (even though the code was actually able to delete the physical file-the instance is still open).
public void DeleteDirectory(string target_dir)
{
string[] files = Directory.GetFiles(target_dir);
string[] dirs = Directory.GetDirectories(target_dir);
List<Process> lstProcs = new List<Process>();
foreach (string file in files)
{
File.SetAttributes(file, FileAttributes.Normal);
lstProcs = ProcessHandler.WhoIsLocking(file);
if (lstProcs.Count == 0)
File.Delete(file);
else // deal with the file lock
{
foreach (Process p in lstProcs)
{
if (p.MachineName == ".")
ProcessHandler.localProcessKill(p.ProcessName);
else
ProcessHandler.remoteProcessKill(p.MachineName, txtUserName.Text, txtPassword.Password, p.ProcessName);
}
File.Delete(file);
}
}
foreach (string dir in dirs)
{
DeleteDirectory(dir);
}
//ProcessStartInfo psi = new ProcessStartInfo();
//psi.Arguments = "/C choice /C Y /N /D Y /T 1 & Del " + target_dir;
//psi.WindowStyle = ProcessWindowStyle.Hidden;
//psi.CreateNoWindow = true;
//psi.FileName = "cmd.exe";
//Process.Start(psi);
//ProcessStartInfo psi = new ProcessStartInfo();
//psi.Arguments = "/C RMDIR /S /Q " + target_dir;
//psi.WindowStyle = ProcessWindowStyle.Hidden;
//psi.CreateNoWindow = true;
//psi.FileName = "cmd.exe";
//Process.Start(psi);
// This is where the failure occurs
//FileSystem.DeleteDirectory(target_dir, DeleteDirectoryOption.DeleteAllContents);
Directory.Delete(target_dir, false);
}
I've left things I've tried commented out in the code above. While I can kill processes attached to the files and delete them, is there a way to kill processes attached to folders, in order to delete them?
Everything online I saw tries to solve this using a loop-check with a delay. This will not work here. I need to kill the file that was opened-which I do-but also ensure the handle is released from the folder so it can also be deleted, at the end. Is there a way to do this?
Another option I considered that will not work:
I thought I might just freeze the "installation" (copying) process by marking that network folder for deletion in the registry and schedule a programmatic reboot of the file server, then re-run afterwards. How to delete Thumbs.db (it is being used by another process) gives this code by which to do this:
[DllImport("kernel32.dll")]
public static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags);
public const int MOVEFILE_DELAY_UNTIL_REBOOT = 0x4;
//Usage:
MoveFileEx(fileName, null, MOVEFILE_DELAY_UNTIL_REBOOT);
But it has in the documentation that If MOVEFILE_DELAY_UNTIL_REBOOT is used, "the file cannot exist on a remote share, because delayed operations are performed before the network is available." And that was assuming it might have allowed a folder path, instead of a file name. (Reference: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365240(v=vs.85).aspx ).

So there are 2 scenarios I wanted to handle - both are where the folder is prevented from being deleted:
1) A user has a file open on their local machine from the application's folder on the file server.
2) An admin has a file open from the application's folder, which they will see while remoted (RDP'ed) into the server.
I've settled on a way forward. If I run into this issue, I figure about all I can do is to either:
1) Freeze the "installation" (copying) process by simply scheduling a programmatic reboot of the file server in the IOException block if I really want to blow away the folder (not ideal and probably overkill, but others running across this same issue may be inspired by this option). The installer will need to be run again to copy the files after the server reboots.
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, out SafeTokenHandle phToken);
LogonUser(userName, domainName, password,
LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
out safeTokenHandle);
try
{
using (WindowsIdentity newId = new WindowsIdentity(safeTokenHandle.DangerousGetHandle()))
{
using (WindowsImpersonationContext impersonatedUser = newId.Impersonate())
{
foreach (Computer pc in selectedList) // selectedList is an ObservableCollection<Computer>
{
string newDir = "//" + pc.Name + txtExtension.Text; // the textbox has /C$/APP_A_DIR in it
if (Directory.Exists(newDir))
{
DeleteDirectory(newDir); // <-- this is where the exception happens
}
}
}
}
}
catch (IOException ex)
{
string msg = "There was a file left open, thereby preventing a full deletion of the previous folder, though all contents have been removed. Do you wish to proceed with installation, or reboot the server and begin again, in order to remove and replace the installation directory?";
MessageBoxResult result = MessageBox.Show(msg, "Reboot File Server?", MessageBoxButton.OKCancel);
if (result == MessageBoxResult.OK)
{
var psi = new ProcessStartInfo("shutdown","/s /t 0");
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
Process.Start(psi);
}
else
{
MessageBox.Show("Copying files...");
FileSystem.CopyDirectory(sourcePath, newDir);
MessageBox.Show("Completed!");
}
}
Reference: How to shut down the computer from C#
OR
2) Ignore it altogether and perform my copy, anyway. The files actually do delete, and I found there's really no problem with having a folder I can't delete, as long as I can write to it, which I can. So this is the one I ultimately picked.
So again, in the IOException catch block:
catch (IOException ex)
{
if (ex.Message.Contains("The process cannot access the file") &&
ex.Message.Contains("because it is being used by another process") )
{
MessageBox.Show("Copying files...");
FileSystem.CopyDirectory(sourcePath, newDir);
MessageBox.Show("Completed!");
}
else
{
string err = "Issue when performing file copy: " + ex.Message;
MessageBox.Show(err);
}
}
Code above leaves out my model for Computer, which just has a Name node in it, and the rest of my Impersonation class, which is based on my own rendition of several different (but similar) code blocks of how they say to do it. If anyone needs that, here are a couple of links to some good answers:
Need Impersonation when accessing shared network drive
copy files with authentication in c#
Related: Cannot delete directory with Directory.Delete(path, true)

Related

How can I programmatically turn off or on 'Windows Features'

When I try to update Windows features; When I update UseShellExecute to "true"; "The Process object must have the UseShellExecute property set to false in order to redirect IO streams." I get an error. When I set it to False; Unable to update. How can I do it ? Do you have any other suggestions?
static void InstallIISSetupFeature()
{
var featureNames = new List<string>() {
"IIS-WebServerRole",
"IIS-WebServer",
"IIS-CommonHttpFeatures",
"IIS-HttpErrors",
"IIS-HttpRedirect",
"IIS-ApplicationDevelopment",
"IIS-Security",
"IIS-RequestFiltering",
"IIS-NetFxExtensibility",
"IIS-NetFxExtensibility45",
"IIS-HealthAndDiagnostics",
"IIS-HttpLogging",
"IIS-LoggingLibraries",
"IIS-RequestMonitor",
"IIS-HttpTracing",
"IIS-URLAuthorization",
"IIS-IPSecurity",
"IIS-Performance",
"IIS-HttpCompressionDynamic",
"IIS-WebServerManagementTools",
"IIS-ManagementScriptingTools",
"IIS-IIS6ManagementCompatibility",
"IIS-Metabase",
"IIS-HostableWebCore","IIS-StaticContent",
"IIS-DefaultDocument",
"IIS-DirectoryBrowsing",
"IIS-WebDAV",
"IIS-WebSockets",
"IIS-ApplicationInit",
"IIS-ASPNET",
"IIS-ASPNET45",
"IIS-ASP",
"IIS-CGI",
"IIS-ISAPIExtensions",
"IIS-ISAPIFilter",
"IIS-ServerSideIncludes",
"IIS-CustomLogging",
"IIS-BasicAuthentication",
"IIS-HttpCompressionStatic",
"IIS-ManagementConsole",
"IIS-ManagementService",
"IIS-WMICompatibility",
"IIS-LegacyScripts",
"IIS-LegacySnapIn",
"IIS-FTPServer",
"IIS-FTPSvc",
"IIS-FTPExtensibility",
"IIS-CertProvider",
"IIS-WindowsAuthentication",
"IIS-DigestAuthentication",
"IIS-ClientCertificateMappingAuthentication",
"IIS-IISCertificateMappingAuthentication",
"IIS-ODBCLogging",
"NetFx4-AdvSrvs",
"NetFx4Extended-ASPNET45",
"NetFx3",
"WAS-WindowsActivationService",
"WCF-HTTP-Activation",
"WCF-HTTP-Activation45",
"WCF-MSMQ-Activation45",
"WCF-NonHTTP-Activation",
"WCF-Pipe-Activation45",
"WCF-TCP-Activation45",
"WCF-TCP-PortSharing45",
"WCF-Services45",
};
ManagementObjectSearcher obj = new ManagementObjectSearcher("select * from Win32_OperatingSystem");
foreach (ManagementObject wmi in obj.Get())
{
string Name = wmi.GetPropertyValue("Caption").ToString();
Name = Regex.Replace(Name.ToString(), "[^A-Za-z0-9 ]", "");
if (Name.Contains("Server 2008 R2") || Name.Contains("Windows 7"))
{
featureNames.Add("IIS-ASPNET");
featureNames.Add("IIS-NetFxExtensibility");
featureNames.Add("WCF-HTTP-Activation");
featureNames.Add("WCF-MSMQ-Activation");
featureNames.Add("WCF-Pipe-Activation");
featureNames.Add("WCF-TCP-Activation");
featureNames.Add("WCF-TCP-Activation");
}
string Version = (string)wmi["Version"];
string Architecture = (string)wmi["OSArchitecture"];
}
foreach (var featureName in featureNames)
{
Run(string.Format("dism/online/Enable-Feature:{0}", featureName));
}
}
static void Run(string arguments)
{
try
{
string systemPath = Path.Combine(Environment.ExpandEnvironmentVariables("%windir%"), "system32");
var dism = new Process();
dism.StartInfo.WorkingDirectory = systemPath;
dism.StartInfo.Arguments = arguments;
dism.StartInfo.FileName = "dism.exe";
dism.StartInfo.Verb = "runas";
dism.StartInfo.UseShellExecute = true;
dism.StartInfo.RedirectStandardOutput = true;
dism.Start();
var result = dism.StandardOutput.ReadToEnd();
dism.WaitForExit();
}
catch (Exception ex)
{
}
}`
I tried to update the feature with dism.exe and cmd.exe, when it gave an authorization error, I used the Verb property
`
Since the use of .Verb = "RunAs" requires .UseShellExecute = true, and since the latter cannot be combined with RedirectStandardOutput = true, you cannot directly capture the elevated process' output in memory.
It seems that the system itself, by security-minded design, prevents a non-elevated process from directly capturing an elevated process' output.
The workaround is to launch the target executable (dism.exe, in your case) indirectly, via a shell, and then use the latter's redirection feature (>) to capture the target executable's output (invariably) in a file, as shown below.
string systemPath = Path.Combine(Environment.ExpandEnvironmentVariables("%windir%"), "system32");
// Create a temp. file to capture the elevated process' output in.
string tempOutFile = Path.GetTempFileName();
var dism = new Process();
dism.StartInfo.WorkingDirectory = systemPath;
// Use cmd.exe as the executable, and pass it a command line via /c
dism.StartInfo.FileName = "cmd.exe" ;
// Use a ">" redirection to capture the elevated process' output.
// Use "2> ..." to also capture *stderr* output.
// Append "2>&1" to capture *both* stdout and stderr in the file targeted with ">"
dism.StartInfo.Arguments =
String.Format(
"/c {0} {1} > \"{2}\"",
"dism.exe", arguments, tempOutFile
);
dism.StartInfo.Verb = "RunAs";
dism.StartInfo.UseShellExecute = true;
dism.Start();
dism.WaitForExit();
// Read the temp. file in which the output was captured...
var result = File.ReadAllText(tempOutFile);
// ... and delete it.
File.Delete(tempOutFile);
First, you can use WindowsPrincipal::IsInRole() to check if you're running elevated.
See Microsoft Learn for details.
Second, this may be one of those cases where using native PS is easier than the cmdlet approach (admittedly, still not great).
If the script is supposed to run on clients as well as server operating systems: use Get-WmiObject or Get-CimInstance to get a reference to what you're running on. ActiveDirectory also has that information (in operatingSystem attribute).
For servers use Get-WindowsFeature in ServerManager module.
For clients use Get-WindowsOptionalFeature with switch -Online in DISM module which, if you indeed need to support OSes older than 6.3.xxxx, can be copied over from a machine that has it and added to $Env:Path before C:\Windows and C:\Windows\System32.
For either platform just pass the list of features to configure.
If in a (binary) cmdlet you have to call external tools then the advantage of them is mostly gone. It may be possible to access Windows CBS using a managed API to avoid this but even then the script based approach gets more results faster, especially since you can just just put together a quick wrapper around dism.exe .

Application: Application Launcher, can't Move directory, it's being used by another process

I'm writing application launcher as a Window Application in C#, VS 2017. Currently, having problem with this piece of code:
if (System.IO.Directory.Exists(extractPath))
{
string[] files = System.IO.Directory.GetFiles(extractPath);
string[] dirs = Directory.GetDirectories(extractPath);
// Copy the files and overwrite destination files if they already exist.
foreach (string s in files)
{
// Use static Path methods to extract only the file name from the path.
var fileName = System.IO.Path.GetFileName(s);
var destFile = System.IO.Path.Combine(oldPath, fileName);
System.IO.File.Move(s, destFile);
}
foreach (string dir in dirs)
{
//var dirSplit = dir.Split('\\');
//var last = dirSplit.Last();
//if (last != "Resources")
//{
var fileName = System.IO.Path.GetFileName(dir);
var destFile = System.IO.Path.Combine(oldPath, fileName);
System.IO.Directory.Move(dir, destFile);
//}
}
}
I'm getting well known error
"The process cannot access the file 'XXX' because it is being used by another process."
I was looking for solution to fix it, found several on MSDN and StackOvervflow, but my problem is quite specific. I cannot move only 1 directory to another, which is Resources folder of my main application:
Here is my explanation why problem is specific:
I'm not having any issues with moving other files from parent directory. Error occurs only when loop reaches /Resources directory.
At first, I was thinking that it's beeing used by VS instantion, in which I've had main app opened. Nothing have changed after closing VS and killing process.
I've copied and moved whole project to another directory. Never opened it in VS nor started via *.exe file, to make sure that none of files in new, copied directory, is used by any process.
Finally, I've restarted PC.
I know that this error is pretty common when you try to Del/Move files, but in my case, I'm sure that it's being used only by my launcher app. Here is a little longer sample code to show what files operation I'm actually doing:
private void RozpakujRepo()
{
string oldPath = #"path\Debug Kopia\Old";
string extractPath = #"path\Debug Kopia";
var tempPath = #"path\ZipRepo\RexTempRepo.zip";
if (System.IO.File.Exists(tempPath) == true)
{
System.IO.File.Delete(tempPath);
}
System.IO.Compression.ZipFile.CreateFromDirectory(extractPath, tempPath);
if (System.IO.Directory.Exists(oldPath))
{
DeleteDirectory(oldPath);
}
if (!System.IO.Directory.Exists(oldPath))
{
System.IO.Directory.CreateDirectory(oldPath);
}
if (System.IO.Directory.Exists(extractPath))
{
string[] files = System.IO.Directory.GetFiles(extractPath);
string[] dirs = Directory.GetDirectories(extractPath);
// Copy the files and overwrite destination files if they already exist.
foreach (string s in files)
{
// Use static Path methods to extract only the file name from the path.
var fileName = System.IO.Path.GetFileName(s);
var destFile = System.IO.Path.Combine(oldPath, fileName);
System.IO.File.Move(s, destFile);
}
foreach (string dir in dirs)
{
//var dirSplit = dir.Split('\\');
//var last = dirSplit.Last();
//if (last != "Resources")
//{
var fileName = System.IO.Path.GetFileName(dir);
var destFile = System.IO.Path.Combine(oldPath, fileName);
System.IO.Directory.Move(dir, destFile);
//}
}
}
string zipPath = #"path\ZipRepo\RexRepo.zip";
ZipFile.ExtractToDirectory(zipPath, extractPath);
}
And now, my questions:
Can it be related to file types (.png, .ico, .bmp) ?
Can it be related to fact, that those resources files are being used like, as, for example .exe file icon in my main application? Or just because those are resources files?
Is there anything else what I'm missing and what can cause the error?
EDIT:
To clarify:
There are 2 apps:
Main Application
Launcher Application (to launch Main Application)
And Resources folder is Main Application/Resources, I'm moving it while I'm doing application version update.
It appeared that problem is in different place than in /Resources directory. Actually problem was with /Old directory, because it caused inifinite recurrence.

Move directory on system when executed C#? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 6 years ago.
Improve this question
I'm looking to move my C# application .EXE when ran to lets say... Documents and then delete from the place which it was executed.
For example, If I ran my .EXE on my desktop before running the program copy itself to the directory "documents" and then delete the one from executed directory (which in my case is desktop) after running the new one in documents.
Process: Run > Move to C://Documents > Start .EXE in documents > Delete the .EXE from the executed directory.
Sorry if this may come across hard to understand for some people I tried my best to specifically state what I wanted to accomplish.
I hope you can write the program in this way which will help.
1) program
i) Check if the program's execution directory is not C:/Documents
then it should copy the folder and put it in C:/Documents
and start the exe inside the documents
ii) else get a running list of the exe and their execution directory
(if its not C:/Documents stop the exe, and delete the execution folder
not sure if this will help , but just this is my thought
There's no way to do this with a single process as the exe which you want to move is going to be running in memory.
You could make the application copy itself, execute the copy, then kill itself.
this will definitely need to be tweaked and is very basic, but hopefully will give you some idea. sorry that it's all statics in a console application, all the methods should be in their own appropriate class.
using System;
using System.Globalization;
using System.IO;
using System.Linq;
namespace StackExchangeSelfMovingExe
{
class Program
{
static void Main(string[] args)
{
// check if we are running in the correct path or not?
bool DoMoveExe = !IsRunningInDocuments();
string runningPath = Directory.GetCurrentDirectory();
if (DoMoveExe)
{
// if we get here then we are not, copy our app to the right place.
string newAppPath = GetDesiredExePath();
CopyFolder(runningPath, newAppPath);
CreateToDeleteMessage(newAppPath, runningPath); // leave a message so new process can delete the old app path
// start the application running in the right directory.
string newExePath = $"{GetDesiredExePath()}\\{System.AppDomain.CurrentDomain.FriendlyName}";
ExecuteExe(newExePath);
// kill our own process since a new one is now running in the right place.
KillMyself();
}
else
{
// if we get here then we are running in the right place. check if we need to delete the old exe before we ended up here.
string toDeleteMessagePath = $"{runningPath}\\CopiedFromMessage.txt";
if (File.Exists(toDeleteMessagePath))
{
// if the file exists then we have been left a message to tell us to delete a path.
string pathToDelete = System.IO.File.ReadAllText(toDeleteMessagePath);
// kill any processes still running from the old folder.
KillAnyProcessesRunningFromFolder(pathToDelete);
Directory.Delete(pathToDelete, true);
}
// remove the message so next time we start, we don't try to delete it again.
File.Delete(toDeleteMessagePath);
}
// do application start here since we are running in the right place.
}
static string GetDesiredExePath()
{
// this is the directory we want the app running from.
string userPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return $"{userPath}\\documents\\MyExe".ToLower();
}
static bool IsRunningInDocuments()
{
// returns true if we are running from within the root of the desired directory.
string runningPath = Directory.GetCurrentDirectory();
return runningPath.StartsWith(GetDesiredExePath());
}
// this copy method is from http://stackoverflow.com/questions/58744/best-way-to-copy-the-entire-contents-of-a-directory-in-c-sharp
public static void CopyFolder(string SourcePath, string DestinationPath)
{
if (!Directory.Exists(DestinationPath))
{
Directory.CreateDirectory(DestinationPath);
}
//Now Create all of the directories
foreach (string dirPath in Directory.GetDirectories(SourcePath, "*",
SearchOption.AllDirectories))
Directory.CreateDirectory(DestinationPath + dirPath.Remove(0, SourcePath.Length));
//Copy all the files & Replaces any files with the same name
foreach (string newPath in Directory.GetFiles(SourcePath, "*.*",
SearchOption.AllDirectories))
File.Copy(newPath, DestinationPath + newPath.Remove(0, SourcePath.Length), true);
}
private static void CreateToDeleteMessage(string newPath, string runningPath)
{
// simply write a file with the folder we are in now so that this folder can be deleted later.
using (System.IO.StreamWriter file =
new System.IO.StreamWriter($"{newPath}\\CopiedFromMessage.txt", true))
{
file.Write(runningPath);
}
}
private static void ExecuteExe(string newExePath)
{
// launch the process which we just copied into documents.
System.Diagnostics.Process.Start(newExePath);
}
private static void KillMyself()
{
// this is one way, depending if you are using console, forms, etc you can use more appropriate method to exit gracefully.
System.Diagnostics.Process.GetCurrentProcess().Kill();
}
private static void KillAnyProcessesRunningFromFolder(string pathToDelete)
{
// kill any processes still running from the path we are about to delete, just incase they hung, etc.
var processes = System.Diagnostics.Process.GetProcesses()
.Where(p => p.MainModule.FileName.StartsWith(pathToDelete, true, CultureInfo.InvariantCulture));
foreach (var proc in processes)
{
proc.Kill();
}
}
}
}

Start a Process from a C# Application impersonating a specific User

I have a .NET c# application,
the business flow (short and to the point) is:
Users make a call to my app which authenticates them by windows authentication.
If the user is a "special user" (business logic part, E.g. some account admin), I impersonate to a "Master Account" in the active directory which has read / write permissions to a shared folder.
I then create folders and files with the impersonated user context --> This works.
But when I try to start a process (bcp.exe for those who care), I can't get it to work!
After many failing attempts , getting many error messages such as "access denied",
and trying to use almost all of the Process.ProcessStartInfo() attributes which should assist me to run a process as a different user, I decided to Post this as a question.
I've read many blogs suggesting the only way to do this is to use the win32 dll and call CreateProcessAsUser() method, but it's just to damn complicated, and I couldn't find any working sample of it.
bottom line question:
How can I start a Process (Process.Start) from a c# app while in impersonation context as the impersonated user?
My code:
private void ExecuteCommand(string backupSource, string backupFilename, string formatFilename)
{
// This works --> Here I'm under impersonated user context
// with read write permissions to the shared folder
if (!Directory.Exists(OutputPath))
{
Directory.CreateDirectory(OutputPath);
}
using (Process p = new Process())
{
p.StartInfo = GetProcessStartInfo(backupSource, backupFilename, formatFilename);
//Here I'm currently getting ""Access Denied" exception"
p.Start();
...
}
}
private ProcessStartInfo GetProcessStartInfo(string backupSource, string backupFilename, string formatFilename)
{
var result = new ProcessStartInfo();
result.UseShellExecute = false;
result.RedirectStandardOutput = true;
result.RedirectStandardError = true;
var file = Path.Combine(PathToExecutable, "bcp.exe");
// #"C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\110\Tools\Binn\bcp.exe";
result.FileName = file;
result.WorkingDirectory = Path.GetDirectoryName(file);
result.LoadUserProfile = true;
result.Domain = "IMPERSONATED USER DOMAIN";
result.UserName = "IMPERSONATED USER NAME";
var ssPwd = new SecureString();
string password = "IMPERSONATED USER PASSWORD";
for (int x = 0; x < password.Length; x++)
{
ssPwd.AppendChar(password[x]);
}
result.Password = ssPwd;
var backupFullFilename = GetFullFileName(backupFilename);
StringBuilder arguments = new StringBuilder(backupSource);
var formatFullFilename = GetFullFileName(formatFilename);
FormatArguments(arguments, backupFullFilename, formatFullFilename);
var argumentsString = arguments.ToString();
result.Arguments = argumentsString;
return result;
}
Edit #1:
I was able to resolve the "Access is denied" exception, by adding the impersonating user to the administrators group on the machine which the application that starts the process runs on.
Now, I'm having a different issue, no exception but seems like the process isn't starting, or exiting right on start, I'm getting exit code 1073741502.
I've read I must use the native win32 api CreateProcessAsUser() instead of System.Diagnostics.Process.Start() but I'm not sure if that's true.
Ideas?
Assistance would be appreciated.

Shadow copy with AppDomain to overwrite exe at runtime

In the following sample app I create a new AppDomain and I execute it with shadow copy enabled. From the new AppDomain I then try to delete (replace) the original main exe. However I get an "access is denied error". Interestingly, after launching the program, from Windows Explorer it is possible to rename the main exe (but not to delete it).
Can shadow copy work for runtime overwriting of the main exe?
static void Main(string[] args)
{
// enable comments if you wanna try to overwrite the original exe (with a
// copy of itself made in the default AppDomain) instead of deleting it
if (AppDomain.CurrentDomain.IsDefaultAppDomain())
{
Console.WriteLine("I'm the default domain");
System.Reflection.Assembly currentAssembly = System.Reflection.Assembly.GetExecutingAssembly();
string startupPath = currentAssembly.Location;
//if (!File.Exists(startupPath + ".copy"))
// File.Copy(startupPath, startupPath + ".copy");
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = Path.GetFileName(startupPath);
setup.ShadowCopyFiles = "true";
AppDomain domain = AppDomain.CreateDomain(setup.ApplicationName, AppDomain.CurrentDomain.Evidence, setup);
domain.SetData("APPPATH", startupPath);
domain.ExecuteAssembly(setup.ApplicationName, args);
return;
}
Console.WriteLine("I'm the created domain");
Console.WriteLine("Replacing main exe. Press any key to continue");
Console.ReadLine();
string mainExePath = (string)AppDomain.CurrentDomain.GetData("APPPATH");
//string copyPath = mainExePath + ".copy";
try
{
File.Delete(mainExePath );
//File.Copy(copyPath, mainExePath );
}
catch (Exception ex)
{
Console.WriteLine("Error! " + ex.Message);
Console.ReadLine();
return;
}
Console.WriteLine("Succesfull!");
Console.ReadLine();
}
You can achive self updating application within a single application with multiple AppDomains. The trick is move application executable to a temporary directory and copy back to your directory, then load the copied executable in a new AppDomain.
static class Program
{
private const string DELETED_FILES_SUBFOLDER = "__delete";
/// <summary>
/// The main entry point for the application.
/// </summary>
[LoaderOptimization(LoaderOptimization.MultiDomainHost)]
[STAThread]
static int Main()
{
// Check if shadow copying is already enabled
if (AppDomain.CurrentDomain.IsDefaultAppDomain())
{
// Get the startup path.
string assemblyPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
string assemblyDirectory = Path.GetDirectoryName(assemblyPath);
string assemblyFile = Path.GetFileName(assemblyPath);
// Check deleted files folders existance
string deletionDirectory = Path.Combine(assemblyDirectory, DELETED_FILES_SUBFOLDER);
if (Directory.Exists(deletionDirectory))
{
// Delete old files from this folder
foreach (var oldFile in Directory.EnumerateFiles(deletionDirectory, String.Format("{0}_*{1}", Path.GetFileNameWithoutExtension(assemblyFile), Path.GetExtension(assemblyFile))))
{
File.Delete(Path.Combine(deletionDirectory, oldFile));
}
}
else
{
Directory.CreateDirectory(deletionDirectory);
}
// Move the current assembly to the deletion folder.
string movedFileName = String.Format("{0}_{1:yyyyMMddHHmmss}{2}", Path.GetFileNameWithoutExtension(assemblyFile), DateTime.Now, Path.GetExtension(assemblyFile));
string movedFilePath = Path.Combine(assemblyDirectory, DELETED_FILES_SUBFOLDER, movedFileName);
File.Move(assemblyPath, movedFilePath);
// Copy the file back
File.Copy(movedFilePath, assemblyPath);
bool reload = true;
while (reload)
{
// Create the setup for the new domain
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = assemblyFile;
setup.ShadowCopyFiles = true.ToString().ToLowerInvariant();
// Create an application domain. Run
AppDomain domain = AppDomain.CreateDomain(setup.ApplicationName, AppDomain.CurrentDomain.Evidence, setup);
// Start application by executing the assembly.
int exitCode = domain.ExecuteAssembly(setup.ApplicationName);
reload = !(exitCode == 0);
AppDomain.Unload(domain);
}
return 2;
}
else
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
MainForm mainForm = new MainForm();
Application.Run(mainForm);
return mainForm.ExitCode;
}
}
}
As it's an interesting use case of MEF, I've bashed out a quick demo of how to hot-swap running code in C#. This is very simple and leaves out a lot of edge cases.
https://github.com/i-e-b/MefExperiments
Notable classes:
src/PluginWatcher/PluginWatcher.cs -- watches a folder for new implementations of a contract
src/HotSwap.Contracts/IHotSwap.cs -- minimal base contract for a hot-swap
src/HotSwapDemo.App/Program.cs -- Does the live code swap
This doesn't lock the task .dlls in the Plugins folder, so you can delete old versions once new ones are deployed.
Hope that helps.
You asked specifically to use ShadowCopy for the update process. If that (and why would it be?) not a fixed requirement, these ones were real eye openers for me:
https://visualstudiomagazine.com/articles/2017/12/15/replace-running-app.aspx
https://www.codeproject.com/Articles/731954/Simple-Auto-Update-Let-your-application-update-i
It comes down to you renaming the target file (which is allowed, even when it is locked since it is running) and then moving/copying the desired file to the now freed destination.
The vs-magazine article is very detailed, including some nifty tricks like finding out if a file is in use by current application (though only for exe, for .dlls and others one has to come up with a solution).

Categories