Sublime Forum

SublimeText3 can't delete folders from side-bar on windows very often

#1

I’ve always had a lot of troubles when it comes to delete folders on SublimeText (windows), that command is not reliable at all, I always end up opening a windows explorer and deleting the folders from there. I know this is not new topic, I’ve already read some old threads asking about this issue but didn’t see any solution so far and this issue hasn’t been addressed for years so let me bring it back again.

Here’s the thing, there is this little delete_folder command living in side_bar.py:

class DeleteFolderCommand(sublime_plugin.WindowCommand):
    def run(self, dirs):

        if len(dirs) == 1:
            message = "Delete Folder %s?" % dirs[0]
        else:
            message = "Delete %d Folders?" % len(dirs)

        if sublime.ok_cancel_dialog(message, "Delete"):
            import Default.send2trash as send2trash
            try:
                for d in dirs:
                    send2trash.send2trash(d)
            except Exception as e:
                import traceback
                traceback.print_exc()
                sublime.status_message("Unable to delete folder")

    def is_visible(self, dirs):
        return len(dirs) > 0

which uses send2trash library, if we look at the windows relevant bits of send2trash the relevant bits are:

def send2trash(path):
    # if not isinstance(path, str):
    #     path = str(path, 'mbcs')
    if not op.isabs(path):
        path = op.abspath(path)
    fileop = SHFILEOPSTRUCTW()
    fileop.hwnd = 0
    fileop.wFunc = FO_DELETE
    fileop.pFrom = LPCWSTR(path + '\0')
    fileop.pTo = None
    fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
    fileop.fAnyOperationsAborted = 0
    fileop.hNameMappings = 0
    fileop.lpszProgressTitle = None
    result = SHFileOperationW(byref(fileop))
    if result:
        msg = "Couldn't perform operation. Error code: %d" % result
        raise OSError(msg)

Here’s some of the tracebacks I’ve got very often when trying to delete a folder:

Traceback (most recent call last):
  File "D:\sources\personal\sublimetext3\Data\Packages\Default\side_bar.py", line 57, in run
    send2trash.send2trash(d)
  File "D:\sources\personal\sublimetext3\Packages\Default.sublime-package\send2trash/plat_win.py", line 54, in send2trash
    raise OSError(msg)
OSError: Couldn't perform operation. Error code: 124

or:

Unable to open /D/sources/personal/sublimetext3/Data/Packages/Default/send2trash/plat_win.py
Traceback (most recent call last):
  File "D:\sources\personal\sublimetext3\Data\Packages\Default\side_bar.py", line 57, in run
    send2trash.send2trash(d)
  File "D:\sources\personal\sublimetext3\Packages\Default.sublime-package\send2trash/plat_win.py", line 54, in send2trash
    raise OSError(msg)
OSError: Couldn't perform operation. Error code: 120

By looking at the docs I see those error codes mean:

DE_ACCESSDENIEDSRC	0x78	Security settings denied access to the source.
DE_INVALIDFILES	0x7C	The path in the source or destination or both was invalid.

Is there any way to fix the code to overcome these errors?

I said “fix” the code cos whether a folder can be deleted from windows explorer but not inside SublimeText that’s a bug. And this is quite a common operation so… :slight_smile:

Thanks in advance

0 Likes

#2

Haven’t seen such errors yet. But saw “error 2” when retrying after a deleted folder didn’t disappear in the side bar. In my case the sidebar was just not updated. :-/ The 0x78 may be a result of at least one file being locked by a background process?

What I find unhappy is to stop deleting the remaining dirs as soon as one fails to be deleted.

import Default.send2trash as send2trash
            try:
                for d in dirs:
                    send2trash.send2trash(d)
            except Exception as e:

Do such errors persist or is it possible to delete the files/folders at a later point of time?

Which files do you try to delete if 0x7c is thrown? I just had a look into the send2trash github repo. They do some path translation from UNC to “short path”, which is not part of ST’s version.

0 Likes

#3

The 0x78 may be a result of at least one file being locked by a background process?

That’s correct, I hadn’t checked the “locked” state of the folder when I wrote my previous thread complaining. When I’d launched unlocker, I’ve realized that particular folder was locked by 6 background ST processes (dunno what these processes were btw, the indexer? some custom code? 3rd party packages? dunno…) but because of it windows explorer wasn’t able to delete the folder until I’ve unlocked it manually.

Just to be clear here, my main concern comes in case you can’t delete a folder in Sublime BUT you can delete it on windows explorer, that’s the BUG which really annoys me. If you can’t delete it on the windows explorer in the first place, well… that also is annoying but wouldn’t be fair to call it a bug.

Do such errors persist or is it possible to delete the files/folders at a later point of time?

As said above, I’ve been able to delete the folder once I’ve unlocked it so guess it was a bad bug report of mine… I’ll try carefully next time to find some deterministic sequence that shows up the bug, although I know it won’t be easy. Maybe some custom code of mine or other packages are locking folders under some conditions… honestly? dunno :confused:

It’d be cool the next time same thing happens knowing how to query some interesting info about the processes that locked the folder/s. Unlocker only gives you {Process name, Path locked, PID, Handle, Process Path} and when the process that locks the folder is SublimeText I don’t whether the PID/Handle will reveal give much more info… so if you’ve got any advice about it please let me know, that way I’ll be able to make a better bug report the next time or even better, figuring out the potential source of locking issues.

I just had a look into the send2trash github repo. They do some path translation from UNC to “short path”, which is not part of ST’s version.

Btw, do you know if ST is using an updated version of send2trash?

0 Likes

#4

I would suspect the other way around.

0 Likes

#5

I didn’t read your thread carefully and therefore didn’t realize the fact you’re able to delete in Explorer :wink:

ST doesn’t use the latest version of send2trash but I guess this is not the issue. The changes I saw wouldn’t effect this issue at all. What I’ve read was the used SHShellOperation function to be deprecated by Microsoft and another issue which might cause the files to be deleted instead of moved to trash in some corner cases. Microsoft advises to use the IFileOperation interface on Win7+ but I did not find any python binding for it but pywin32.

BTW: I ran into the same error 120 issue yesterday by accident, when I tried to delete a folder with nearly 1.000 files in it.

0 Likes

#6

It’s trivial to call win32 API with ctypes. Unless you need an entire suite of methods, pywin32 is unnecessary.

I just wonder whether we would need windows XP compatibility.

0 Likes

#7

Agree. I like the simplicity of that function call, too. Don’t like the way the Windows API becomes more and more complicated without adding any value. The question is, what did they change behind the scenes? The most obvious change of IFileOperation seems support for paths longer than 260 chars.

Just did some further investigation on the folder I’ve experienced that issue yesterday.

It is a folder containing html and xml files.

   + hlp
      + deu
         + folder1
            - 62x html files
         + folder2
            - 1.254x html files
         - folder1_index.xml
         - folder2_index.xml

This folder is part of a project.

If I open this project in a new and only ST instance without any views open, I can use Delete from Side bar to remove the whole folder tree successfully.

As soon as I open one file located within these folders (e.g. folder1_index.xml) error 120 is thrown when deleting hlp folder even after closing all views again. From this point on Windows Explorer can’t delete that folder as well.

  1. I need to close ST to be able to remove hlp directly.
  2. I can delete the folder deu and hlp after each other via side bar and Explorer.

I’ve tried to track down the issue using SysInternals procmon, but can’t see any difference between successful or failed attempt. In both cases the whole directory tree is enumerated and SetDispositionInformationFile with argument Delete: True is called for each file. In both cases this function is called for the directories at the beginning and the end of the command chain with the result NOT EMPTY.

By opening a file ST somehow locks the containing folder.

Remarks:

  1. I guess Windows Explorer on Win10 uses IFileOperation API already, so the issue is not related with SHFileOperation only.
  2. I checked for the indexer to be idle before trying to delete the folders.
  3. The issue exists in vanilla install, too, and is therefore not caused by any 3rd party package/plugin
1 Like

#8

Tried to summarize everything and created an issue

0 Likes

#9

@deathaxe Thanks for all your research and for opening that issue showing a deterministic way to show some of the limitations of delete_folder, that’s definitely helpful.

Today I’ve discovered there is available https://docs.python.org/3/library/os.html#os.chflags on python but unfortunately it’s only available on unix. The idea would be find out how to unlock file/folders properly (in a multiplatform way), before trying to delete them, that way the delete_folder command will become a little bit more reliable. Right now I’m trying to figure out how to unlock files/folders on windows… I’ll continue googling about it, in the meantime I’ve opened an SO thread.

0 Likes

#10

I guess it’s just Sublime Text which does not close all handles after reading the file correctly. I did some investigation on it which I will add to the issue.

0 Likes

#11

I’ve come up with a partial solution (windows):

from collections import defaultdict
import ctypes
import Default.send2trash as send2trash
import os
import re
import sublime
import sublime_plugin
import subprocess
import threading
import time

class ProgressBar:

    def __init__(self, label, width=10):
        self.label = label
        self.width = width

    def start(self):
        self.done = False
        self.update()

    def stop(self):
        sublime.status_message("")
        self.done = True

    def update(self, status=0):
        if self.done:
            return
        status = status % (2 * self.width)
        before = min(status, (2 * self.width) - status)
        after = self.width - before
        sublime.status_message("{} [{}={}]".format(
            self.label, " " * before, " " * after
        ))
        sublime.set_timeout_async(lambda: self.update(status + 1), 100)


class DeleteFolderCommand(sublime_plugin.WindowCommand):

    def run(self, dirs):
        t = threading.Thread(target=self.callback, args=(dirs,))
        t.start()

    def callback(self, dirs):
        if len(dirs) == 1:
            message = "Delete Folder %s?" % dirs[0]
        else:
            message = "Delete %d Folders?" % len(dirs)

        if sublime.ok_cancel_dialog(message, "Delete"):
            for d in dirs:
                try:
                    send2trash.send2trash(d)
                except Exception as e:
                    if not self.is_admin():
                        return sublime.error_message(
                            "Can't unlock the folder, you need to run sublime as admin"
                        )

                    self.unlock_and_delete(d)

    def unlock_and_delete(self, d):
        try:
            s = time.time()
            pb = ProgressBar("Unlocking folder {}".format(d))
            pb.start()
            cmd = ['handle', d, '-nobanner']
            out, err = self.check_popen_with_errors(cmd)
            pb.stop()
            dct = self.pid_handles(out).items()

            if dct:
                msg = "Folder is locked, do you still want to proceed?"
                if not sublime.ok_cancel_dialog(msg, "Delete"):
                    return

            for pid, handles in dct:
                for handle in handles:
                    cmd = [
                        'handle',
                        '-c', handle,
                        '-p', pid,
                        '-y',
                        '-nobanner'
                    ]
                    out, err = self.check_popen_with_errors(cmd)

            send2trash.send2trash(d)
        except Exception as e:
            import traceback
            traceback.print_exc()

    def is_admin(self):
        try:
            is_admin = os.getuid() == 0
        except AttributeError:
            is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0

        return is_admin

    def pid_handles(self, text):
        regex = r"^[^:]+: (\d+)[^:]+:[^:]+?(\w+):"
        dct = defaultdict(list)

        for match in re.finditer(regex, text, re.MULTILINE):
            dct[match.group(1)].append(match.group(2))

        return dct

    def check_popen_with_errors(self, cmd=None):
        startupinfo = None
        if os.name == "nt":
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW

        return subprocess.Popen(
            cmd,
            startupinfo=startupinfo,
            stderr=subprocess.PIPE,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.PIPE,
            universal_newlines=True
        ).communicate()

    def is_visible(self, dirs):
        return len(dirs) > 0

Main problems about the above solution:

  • Relies on a 3rd party tool, you need to download and add to path sysinternals handle
  • You need to run sublime as admin, otherwise handle.exe will fail miserably when trying to unlock the folders :frowning:
  • When running sublime as admin checking the opened handles from processes is really fast but if you try to do so as normal user for some reason handle.exe would take quite a lot of time to achieve the task (~10-20s on my box with few processes running)… That’s why the current code is checking whether sublime is already running in admin-mode, that way won’t execute a long-running handle process that won’t be able to close handles.

Possible workarounds:

PS. Handy ProgressBar class borrowed from @randy3k AutomaticPackageReloader, thanks about it!

1 Like