Wednesday, December 31, 2014

Episode #180: Open for the Holidays!

Not-so-Tiny Tim checks in with the ghost of Christmas present:

I know many of you have been sitting on Santa's lap wishing for more Command Line Kung Fu. Well, we've heard your pleas and are pushing one last Episode out before the New Year!

We come bearing a solution for a problem we've all encountered. Ever try to delete or modify a file and receive an error message that the file is in use? Of course you have! The real problem is trying to track down the user and/or process that has the file locked.

I have a solution for you on Windows, "openfiles". Well, sorta. This time of year I can't risk getting on Santa's bad side so let me add the disclaimer that it is only a partial solution. Here's what I mean, let's look for open files:

C:\> openfiles

INFO: The system global flag 'maintain objects list' needs
      to be enabled to see local opened files.
      See Openfiles /? for more information.


Files opened remotely via local share points:
---------------------------------------------

INFO: No shared open files found.

By default when we run this command it gives us an error that we haven't enabled the feature. Wouldn't it be nice if we could simply turn it on and then look at the open files. Yes, it would be nice...but no. You have to reboot. This present is starting to look a lot like a lump of coal. So you need know that you will encounter the problem before it happens so you can be ready for it. Bah-Humbug!

To enable "openfile" run this command:

C:\> openfile /local on

SUCCESS: The system global flag 'maintain objects list' is enabled.
         This will take effect after the system is restarted.

...then reboot.

Of course, now that we've rebooted the file will be unlocked, but we are prepared for next time. So next time when it happens we can run this command to see the results (note: if you don't specify a switch /query is implied):

C:\> openfiles /query

Files Opened Locally:
---------------------

ID    Process Name         Open File (Path\executable)                       
===== ==================== ==================================================
8     taskhostex.exe       C:\Windows\System32
224   taskhostex.exe       C:\Windows\System32\en-US\taskhostex.exe.mui
296   taskhostex.exe       C:\Windows\Registration\R00000000000d.clb
324   taskhostex.exe       C:\Windows\System32\en-US\MsCtfMonitor.dll.mui
752   taskhostex.exe       C:\Windows\System32\en-US\winmm.dll.mui
784   taskhostex.exe       C:\..\Local\Microsoft\Windows\WebCache\V01tmp.log
812   taskhostex.exe       C:\Windows\System32\en-US\wdmaud.drv.mui
...

Of course, this is a quite long list. You can use use "find" or "findstr" to filter the results, but be aware that long file names are truncated (see ID 784). You can get a full list by changing the format with "/fo LIST". However, the file name will be on a separate line from the owning process and neither "find" nor "findstr" support context.

Another oddity, is that there seems to be duplicate IDs.

C:\> openfiles /query | find "888"
888   chrome.exe           C:\Windows\Fonts\consola.ttf
888   Lenovo Transition.ex C:\..\Lenovo\Lenovo Transition\Gui\yo_btn_g3.png
888   vprintproxy.exe      C:\Windows\Registration\R00000000000d.clb

Different processes with different files, all with the same ID. This means that when you disconnect the open file you better be careful.

Speaking of disconnecting the files, we can do just that with the /disconnect switch. We can disconnect by ID (ill advised) with the /id switch. We can also disconnect all the files based on the user:

C:\> openfiles /disconnect /a jacobmarley

Or the file name:

C:\> openfiles /disconnect /op "C:\Users\tm\Desktop\wishlist.txt" /a *

Or even the directory:

C:\> openfiles /disconnect /op "C:\Users\tm\Desktop\" /a *

We can even run this against a remote system with the /s SERVERNAME option.

This command is far from perfect, but it is pretty cool.

Sadly, there is no built-in capability in PowerShell to do this same thing. With PowerShell v4 we get Get-SmbOpenFile and Close-SmbOpenFile, but they only work on files opened over the network, not on files opened locally.

Now it is time for Mr. Scrooge Pomeranz to ruin my day by using some really useful, built-in, and ENABLED features of Linux.

It's a Happy Holiday for Hal:

Awww, Tim got me the nicest present of all-- a super-easy Command-Line Kung Fu Episode to write!

This one's easy because Linux comes with lsof, a magical tool surely made by elves at the North Pole. I've talked about lsof in several other Episodes already but so far I've focused more on network and process-related queries than checking objects in the file system.

The simplest usage of lsof is checking which processes are using a single file:

# lsof /var/log/messages
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
rsyslogd  1250 root    1w   REG    8,3 13779999 3146461 /var/log/messages
abrt-dump 5293 root    4r   REG    8,3 13779999 3146461 /var/log/messages

Here we've got two processes that have /var/log/messages open-- rsyslogd for writing (see the "1w" in the "FD" column, where the "w" means writing), and abrt-dump for reading ("4r", "r" for read-only).

You can use "lsof +d" to see all open files in a given directory:

# lsof +d /var/log
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
rsyslogd  1250 root    1w   REG    8,3 14324534 3146461 /var/log/messages
rsyslogd  1250 root    2w   REG    8,3   175427 3146036 /var/log/cron
rsyslogd  1250 root    5w   REG    8,3  1644575 3146432 /var/log/maillog
rsyslogd  1250 root    6w   REG    8,3     2663 3146478 /var/log/secure
abrt-dump 5293 root    4r   REG    8,3 14324534 3146461 /var/log/messages

The funny thing about "lsof +d" is that it only shows you open files in the top-level directory, but not in any sub-directories. You have to use "lsof +D" for that:

# lsof +D /var/log
COMMAND     PID   USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
rsyslogd   1250   root    1w   REG    8,3 14324534 3146461 /var/log/messages
rsyslogd   1250   root    2w   REG    8,3   175427 3146036 /var/log/cron
rsyslogd   1250   root    5w   REG    8,3  1644575 3146432 /var/log/maillog
rsyslogd   1250   root    6w   REG    8,3     2663 3146478 /var/log/secure
httpd      3081 apache    2w   REG    8,3      586 3146430 /var/log/httpd/error_log
httpd      3081 apache   14w   REG    8,3        0 3147331 /var/log/httpd/access_log
...

Unix-like operating systems track open files on a per-partition basis. This leads to an interesting corner-case with lsof: if you run lsof on a partition boundary, you get a list of all open files under that partition:

# lsof /
COMMAND     PID      USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
init          1      root  cwd    DIR    8,3     4096        2 /
init          1      root  rtd    DIR    8,3     4096        2 /
init          1      root  txt    REG    8,3   150352 12845094 /sbin/init
init          1      root  DEL    REG    8,3           7340061 /lib64/libnss_files-2.12.so
init          1      root  DEL    REG    8,3           7340104 /lib64/libc-2.12.so
...
# lsof / | wc -l
3500

Unlike Windows, Linux doesn't really have a notion of disconnecting processes from individual files. If you want a process to release a file, you kill the process. lsof has a "-t" flag for terse output. In this mode, it only outputs the PIDs of the matching processes. This was designed to allow you to easily substitute the output of lsof as the arguments to the kill command. Here's the little trick I showed back in Episode 22 for forcibly unmounting a file system:

# umount /home
umount: /home: device is busy
# kill $(lsof -t /home)
# umount /home

Here we're exploiting the fact that /home is a partition mount point so lsof will list all processes with files open anywhere in the file system. Kill all those processes and Santa might leave coal in your stocking next year, but you'll be able to unmount the file system!