Detecting And Logging File Locks With Python
A process that keeps a file open can delay deployments, block log rotation, interrupt backups, or leave a shared document apparently “in use”. On Linux, the operating system tracks several kinds of advisory locks, but the information is spread across kernel interfaces and command-line tools rather than exposed through one simple Python function.
A practical monitoring script can inspect /proc/locks, identify the device and inode associated with a target path, and write a timestamped event whenever a process begins or stops holding a lock. This approach is lightweight enough for a developer workstation and useful on production servers running Python services, cron jobs, or shell automation.
The distinction between an open file and a locked file matters. A process may have a file descriptor open without preventing another process from using the file. Conversely, an advisory lock may block cooperating applications while the kernel still permits an unrelated program to write. The script therefore needs to report what the operating system knows, rather than assume that every open descriptor represents contention.
This is particularly useful in Australian development teams spread across Sydney, Melbourne, Brisbane, and Perth, where a small diagnostic utility can avoid a lengthy handover between time zones. It can also provide evidence when a nightly job, document synchronisation task, or local backup behaves differently from an interactive test.
Understanding Linux File Locking
Linux commonly exposes POSIX record locks, BSD flock locks, and open-file-description locks. They do not always interact with one another, so a script that successfully acquires a flock lock has not necessarily proved that no POSIX lock exists. /proc/locks offers a useful kernel-level view of advisory locks and normally includes the lock type, access mode, owning process ID, device, inode, and byte range.
The device and inode pair is important because a pathname is only a directory entry. A process may reach the same underlying file through a hard link, a symbolic link, or a renamed path. Comparing os.stat() values with /proc/locks avoids relying on the exact spelling of the filename.
The interface is Linux-specific and generally requires a mounted proc filesystem. It also describes advisory locks, not every possible application-level reservation. A database, editor, or network filesystem may maintain its own locking protocol, so the monitor should be presented as an operating-system diagnostic rather than an absolute authority.
Building The Lock Detector
The following script resolves a path, converts its device number into the major and minor values shown by /proc/locks, and parses matching records. It records the process ID, lock mode, lock category, and byte range. A state set prevents repeated messages every polling cycle.
#!/usr/bin/env python3
import logging
import os
import signal
import time
TARGET = "/var/tmp/example.data"
INTERVAL = 2.0
running = True
logging.basicConfig(
filename="/var/log/file-lock-monitor.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
def stop(_signum, _frame):
global running
running = False
def locks_for(path):
stat_result = os.stat(path)
device = f"{os.major(stat_result.st_dev):02x}:{os.minor(stat_result.st_dev):02x}"
inode = str(stat_result.st_ino)
found = []
try:
with open("/proc/locks", encoding="ascii") as locks_file:
for line in locks_file:
fields = line.split()
if len(fields) < 8:
continue
# Typical fields: id, class, scope, mode, pid, dev:inode, start, end
if fields[5] != f"{device}:{inode}":
continue
found.append({
"id": fields[0].rstrip(":"),
"kind": fields[1],
"scope": fields[2],
"mode": fields[3],
"pid": fields[4],
"start": fields[6],
"end": fields[7],
})
except FileNotFoundError:
logging.error("/proc/locks is unavailable")
return found
signal.signal(signal.SIGINT, stop)
signal.signal(signal.SIGTERM, stop)
previous = set()
while running:
try:
current_records = locks_for(TARGET)
current = {
tuple(record[item] for item in
("id", "kind", "scope", "mode", "pid", "start", "end"))
for record in current_records
}
for record in current - previous:
logging.info("LOCK ACQUIRED path=%s details=%s", TARGET, record)
for record in previous - current:
logging.info("LOCK RELEASED path=%s details=%s", TARGET, record)
previous = current
except FileNotFoundError:
logging.warning("Target does not exist: %s", TARGET)
except PermissionError as error:
logging.error("Cannot inspect %s: %s", TARGET, error)
time.sleep(INTERVAL)
The fields in /proc/locks are kernel output, so defensive parsing is worthwhile. Kernel versions and lock implementations can add details, and a process may exit before the monitor examines /proc/<pid>. The script logs the PID recorded with the lock, but it should not assume that PID remains valid after a release event.
Making The Log Useful
A plain event line is more valuable when it includes an absolute path, UTC or clearly labelled local time, the process ID, and the lock mode. Python’s logging module supplies timestamps, while a separate lookup of /proc/<pid>/comm can add the executable name. That lookup should be treated as best effort because processes can disappear between reading the lock table and reading their metadata.
For a long-running service, use RotatingFileHandler or send records to journald rather than allowing a diagnostic log to fill the disk. On a server in Adelaide or Canberra, local time may be convenient for an operations team, but UTC prevents confusion when daylight saving changes affect Sydney, Melbourne, or Hobart. Keep the chosen convention in the log format and runbook.
A state-change model avoids thousands of duplicate entries. It reports acquisition and release events, while a periodic heartbeat can confirm that the monitor itself remains alive. If a lock lasts beyond a threshold, emit a warning with the duration, but avoid treating every long-lived lock as an error; some applications intentionally hold locks for the life of a transaction.
Handling Real File Paths
A file can be renamed or deleted while a process still holds it. In that case, os.stat(TARGET) may fail even though /proc/locks still contains a record for the inode. A more advanced monitor can retain the last known device and inode, continue watching that identity, and inspect /proc/<pid>/fd to find a descriptive path such as (deleted).
Symlinks also deserve deliberate handling. os.stat() follows the link and identifies the target, which is normally what an administrator wants. os.lstat() would instead identify the link itself, producing a mismatch with the locked regular file. Hard links are handled naturally because they share the same inode.
When the target is on NFS, a container, or a clustered filesystem, lock visibility may differ from a local ext4 or XFS volume. Namespaces can also make process IDs and paths appear differently to the host and the monitored process. Test the utility in the same namespace and mount environment as the service being investigated.
Combining Python With Existing Tools
Python is well suited to polling, state tracking, structured logging, and alert integration, while established utilities can provide a second view. lslocks displays kernel locks in a human-friendly format, fuser identifies processes using a path, and lsof reports open files and, where available, lock information. Comparing those results helps expose whether the problem is a lock, an ordinary open descriptor, or an application-level protocol.
For teams diagnosing shared documents, SharePoint file views can provide useful context about how a collaboration platform presents files, while the Linux monitor explains what is happening on a local worker or synchronisation host. The two perspectives should not be conflated: a cloud service’s “checked out” state may exist independently of a Linux kernel lock.
A small command-line wrapper can select a path, polling interval, log destination, and output format without changing the detection code. That makes the utility easier to deploy through Ansible or a systemd service, including on modest VPS instances commonly used by Australian open-source projects.
Security And Operational Boundaries
The log may reveal filenames, usernames, process IDs, and application behaviour. Under Australia’s Privacy Act 1988, organisations should consider whether operational records contain personal information and apply suitable access controls, retention periods, and disposal practices. Restrict the log file to administrators, avoid recording file contents, and do not collect more process metadata than the investigation requires.
Run the monitor with the least privilege needed to read the target and /proc/locks. Root access may expose more process information, but it also increases the impact of a compromise. Validate paths, avoid shell interpolation, and use Python’s subprocess APIs with argument lists if the script later invokes lslocks or another diagnostic command.
The finished utility is best treated as an observability component: it records evidence, helps identify the responsible process, and supports a targeted fix. It does not forcibly remove a lock or terminate an application, which keeps the monitoring action safe for production Linux systems.
