VS Code hangs on file access with WSL 2

Cartoon of an annoyed grey-haired man, wearing spectacles, sat at his laptop, pulling his hair out.

If you’re running Visual Studio Code with WSL 2, you’ll find you get the best performance if your files are within the WSL filesystem. (I’ve written about this separately.)

You might still experience VS Code grinding to a halt, attempting to access files however. One symptom at this point is that if you run df or ls /mnt within WSL, this will hang. You may not even be able to exit with Ctrl-C or kill -9.

It seems that an I/0 lockup can occur with network/remote shares. One of the easiest ways to diagnose this is to strace df. This will show you the problematic share causing the hang. E.g.:

[rob@PC:/]$ strace df
execve("/usr/bin/df", ["df"], 0x7fffd3bb8800 /* 34 vars */) = 0
brk(NULL)                               = 0x55c6c8ef8000
...
newfstatat(AT_FDCWD, "/mnt/w", {st_mode=S_IFDIR|0544, st_size=4096, …}, 0) = 0

In my case I had the Windows W: drive connected to another Linux machine via, using SSHFS-Win. I don’t ever need to access this drive in WSL, but it’s available because I’m using automount.

To keep automount enabled, but exclude one network drive, for example:

  • Create an empty directory, which we’ll mount as a dummy: mkdir /mnt/empty
  • Ensure fstab mounting is enabled. In /etc/wsl.conf, in the [automount] section, you need a line: mountFsTab=true
  • Then in /etc/fstab, mount the folder that would normally be linked to the Windows drive to the empty folder instead. E.g.: /mnt/emptyW /mnt/w none bind 0 0
  • Restart WSL at a Windows command/terminal prompt: wsl --shutdown & wsl

The directory in question, that previously would have shown the contents of the remote folder, should now be empty.

If you have more than one such folder, you may need to repeat the steps, starting with strace df. For me, this restored VS Code’s filesystem performance.

Windows Defender exclusions with Ansible

Baby robot types commands into a computer. There is an antivirus shield icon on the computer screen.

There isn’t yet an official Ansible module for managing Windows Defender exclusions. If you’re committed to idempotency, and not sure how to add a path to Windows Defender’s exclusions list, read on!

I’ll get straight into the task:

- name: Exclude directories from Windows Defender
  ansible.windows.win_powershell:
    script: |
      $Ansible.Changed = $false
      
      # Check if the folder is already excluded
      $excluded = Get-MpPreference | Select-Object -ExpandProperty ExclusionPath | Where-Object { $_ -eq "{{ item }}" }

      if ($null -eq $excluded) {
        # Add the folder to the exclusion list
        try {
          Add-MpPreference -ExclusionPath "{{ item }}"
          $Ansible.Changed = $true
        } catch {
          $Ansible.Failed = $true
        }
      }
  loop:
    - C:\foo\bar.exe
    - H:\baz\qux

To break it down:

  • loop: Pass the file/directory paths that you wish to exclude, as a list. Add-MpPreference does not accept wildcards, so you may need to be verbose, or add exclusions at the directory level rather than file level. These are the “target exclusions” referred to below.
  • ansible.windows.win_powershell: This module gives us the ability with custom PowerShell scripts to tell Ansible explicitly whether the task failed or resulted in a change.
  • $Ansible.Changed = $false: Ansible uses this variable to detect whether or not the task resulted in a change. We initialise it to $false, since no change has occurred yet.
  • Get-MpPreference: This PowerShell cmdlet retrieves lots of Windows Defender configuration imformation.
  • Select-Object -ExpandProperty ExclusionPath: Get the existing exclusions from the previous command, as a list.
  • Where-Object { $_ -eq "{{ item }}" }: Look for our target exclusion in the list.
  • if ($null -eq $excluded) {: If we didn’t find the target exclusion, run the next bit of code. If we did find the exclusion, we don’t execute any more code. the script will end with $Ansible.Changed still set to $false, which is what we want, to preserve idempotency.
  • try { ... } catch { ... }: If we fail to add the exclusion, PowerShell throws an error, so we wrap this in try..catch so we can handle that failure.
  • Add-MpPreference -ExclusionPath "{{ item }}": Add the target exclusion to Windows Defender’s exclusions list. Note that if this fails, PowerShell will immediately jump to the code in the catch block.
  • $Ansible.Changed = $true: We’ll only reach this line if (a) the target exclusion wasn’t already in the list and (b) adding the exclusion succeeded. Hence we set $Ansible.Changed to $true.
  • $Ansible.Failed = $true: We reach this code if Add-MpPreference failed, hence we set $Ansible.Failed to $true.

I hope this is helpful! If you know of a better or more elegant way of doing this, let me know in the comments. 🙂