删除有人打开文件的目录

Delete a directory where someone has opened a file

我正在尝试以编程方式删除和替换应用程序 "App A" 的内容,使用 "installer" 程序,它只是一个自定义 WPF .exe 应用程序,我们将调用 "App B"。 (我的问题涉及 "App B" 中的代码。)

GUI设置(不是特别重要)
App B 有一个 GUI,用户可以在其中选择要将 App A 复制到的计算机名称。管理员使用文件选择器通过单击 "App A.exe" 填写本地计算机上的源目录路径。还有用于用户名和密码的文本框,因此管理员可以输入他们为应用程序 A 提供服务的目标文件服务器的凭据 - 代码使用这些模拟用户以防止权限问题。 "Copy" 按钮启动例程。

终止应用程序 A、文件进程并执行文件删除
Copy 例程首先终止域中所有计算机上的 "App A.exe" 进程,以及 explorer.exe,以防它们打开了 App A 的资源管理器文件夹。显然,这将在下班后完成,但有些人在回家之前可能仍然打开着东西并锁上了他们的机器。这确实是我要解决的问题的基础。

在复制更新文件之前,我们要删除整个旧目录。为了删除目录(及其子目录),必须删除其中的每个文件。 但是说他们从 App A 的文件夹中打开了一个文件。代码会在删除任何文件之前找到任何文件上的任何锁定过程(使用 Eric J. 在 [=17= 的回答中的代码) ] ),它会在 运行 所在的任何计算机上终止该进程。如果是本地的,它只使用:

public static void localProcessKill(string processName)
{
    foreach (Process p in Process.GetProcessesByName(processName))
    {
        p.Kill();
    }
}

如果是远程,则使用 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();
        }
    }
}

然后就可以删除文件了。一切顺利。

目录删除失败
在我下面的代码中,它正在执行文件的递归删除,并且做得很好,直到 Directory.Delete(),它会说 The process cannot access the file '\\SERVER\C$\APP_A_DIR' because it is being used by another process,因为我试图删除目录,而我有一个文件仍然从它打开(即使代码实际上能够删除物理文件 - 实例仍然打开)。

    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);
    }

我已经在上面的代码中注释掉了我尝试过的内容。虽然我可以终止附加到文件的进程并删除它们,有没有办法终止附加到文件夹的进程以便删除它们?

我在网上看到的所有内容都试图通过延迟循环检查来解决这个问题。这在这里不起作用。我需要终止打开的文件——我这样做了——但还要确保从文件夹中释放句柄,以便最后也可以将其删除。有办法吗?

另一个我认为行不通的选项: 我想我可以通过在注册表中标记要删除的网络文件夹来冻结 "installation"(复制)过程,并安排以编程方式重新启动文件服务器,然后再重新 运行。 How to delete Thumbs.db (it is being used by another process) 给出了执行此操作的代码:

[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);

但它在文档中说如果使用 MOVEFILE_DELAY_UNTIL_REBOOT,"the file cannot exist on a remote share, because delayed operations are performed before the network is available." 并且假设它可能允许文件夹路径,而不是文件名。 (参考:https://msdn.microsoft.com/en-us/library/windows/desktop/aa365240(v=vs.85).aspx)。

所以有 2 种情况我想处理 - 都是防止文件夹被删除的地方:

1) 用户在其本地计算机上从文件服务器上的应用程序文件夹中打开了一个文件。

2) 管理员从应用程序的文件夹中打开了一个文件,他们将在远程(RDP)到服务器时看到该文件。

我已经确定了前进的方向。如果我 运行 进入这个问题,我想我能做的就是:

1) 如果我 真的 想要删除文件夹,只需在 IOException 块中安排文件服务器的编程重启即可冻结 "installation"(复制)过程(不理想而且可能矫枉过正,但其他 运行 遇到同样问题的人可能会受到此选项的启发)。安装程序将需要再次 运行 以在服务器重新启动后复制文件。

[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!");
    }
}

参考: How to shut down the computer from C#



2) 完全忽略它并执行我的副本,无论如何。这些文件确实会删除,而且我发现拥有一个我无法删除的文件夹真的没有问题,只要我可以写入它,我可以。所以这就是我最终选择的。

再一次,在 IOException 捕获块中:

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);
    }
}

上面的代码省略了我的 Computer 模型,其中只有一个 Name 节点,以及我的模仿 class 的其余部分,它基于我自己的演绎他们所说的几个不同(但相似)的代码块。如果有人需要,这里有几个指向一些好的答案的链接:

Need Impersonation when accessing shared network drive

copy files with authentication in c#

相关: Cannot delete directory with Directory.Delete(path, true)