Delete a directory where someone has opened a file
Asked Answered
L

1

3

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

Latinalatinate answered 25/1, 2017 at 0:46 Comment(4)
You could logoff the logged on users. You're already being kinda "rude" :), by killing explorer.exe, so logging the user off would solve all your problems. That's better then rebooting since you run the risk of a server not coming back up for whatever reason.Sharilyn
Yeah, unfortunately we have tens of thousands of users in the domain and only a small subset will be users of this application. I think I can safely kill explorer.exe (since it will restart, and people can re-open folders easily)... even though our policy is everyone should log-off when they leave, and we actually reboot machines when installing updates, I'm not sure a domain-wide log-off is the approach we'd want, but might be feasible to go to our user's table in our database to find them. Can we do this kind of selective approach? How do you map users to logged-in computers?Latinalatinate
Wait.. the app runs on MachineX and users RDP into MachineX? Or they run the app remotely by UNC? Or the app lives on client machines and some component points to MachineX?Sharilyn
Users connect to it by UNC path. The app will be stored on a file server. We'll have a shortcut on their desktop that points to it. So it's probably not a huge concern they'll discover the folder where it is hosted and have a file open from it, as they'd have to look at the shortcut's properties in order to even get to it. But it is possible an IT team member might be looking at a log, leave open a config file, etc.Latinalatinate
L
1

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)

Latinalatinate answered 27/1, 2017 at 20:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.