Skip to main content
Task Automation Scripts

When Your Automation Script Ignores a Critical File: 3 Checks to Run First

You've seen it happen. A cron job or scheduled task that's been running for months suddenly ignores a critical file. The output is incomplete, but the script exits with zero errors. No one notices until a customer complains or a report doesn't balance. The file wasn't deleted; it just wasn't processed. Before you blame the script, check these three things first. They're dead simple but often overlooked. 1. Why Your Script Might Skip Files (and Why It's a Big Deal) The Real Cost of a Missed File in Production You run your automation script. Logs show zero errors. Then a teammate asks where yesterday's invoice batch went—and your stomach drops. The file never moved, never transformed, never loaded. That silence is expensive. A single skipped CSV on a payment run means manual re-entry, a late supplier remittance, or worse: a customer billed the wrong amount.

图片

You've seen it happen. A cron job or scheduled task that's been running for months suddenly ignores a critical file. The output is incomplete, but the script exits with zero errors. No one notices until a customer complains or a report doesn't balance. The file wasn't deleted; it just wasn't processed. Before you blame the script, check these three things first. They're dead simple but often overlooked.

1. Why Your Script Might Skip Files (and Why It's a Big Deal)

The Real Cost of a Missed File in Production

You run your automation script. Logs show zero errors. Then a teammate asks where yesterday's invoice batch went—and your stomach drops. The file never moved, never transformed, never loaded. That silence is expensive. A single skipped CSV on a payment run means manual re-entry, a late supplier remittance, or worse: a customer billed the wrong amount. I've watched a missed log archive cascade into a three-hour fire drill because nobody caught the gap until the nightly reconciliation failed. The script "worked"—except it didn't.

What stings most is the asymmetry. A script that processes 9,999 files correctly and skips one looks like a victory in the logs. That last file? Vanished without a trace. No alert, no partial-failure metric—just an empty seat at the table. The debugging time eats hours because you start by doubting the file existed at all. Wrong order.

Common Scenarios That Trigger the Skip

Network shares are notorious. The script polls a Windows folder where an upstream service writes temporary .tmp files, renames them, and then—sometimes—leaves a lock handle hanging. Your pattern matches *.csv perfectly, but that lock prevents a read. The file is technically present; your script sees a ghost. I've debugged one where the temp file survived for 400 milliseconds after the rename, which was exactly when our poller ran. That 400 ms cost a developer a full day of bisecting log timestamps.

Then there are the locked documents—office files left open on someone's desktop, PDFs being indexed by antivirus, or database exports that hold an exclusive lock for three seconds longer than your watch cycle. Quick reality check: most out-of-the-box file watchers treat "locked" as "don't touch, try later" and then never try later. The retry logic never existed.

Another easy miss: hidden files or system attributes. On Linux, a dot-prefix file in a directory scanned by glob('*.json') won't match. On Windows, a file with the Hidden flag set might get filtered by the OS enumeration call. Your pattern looks right on paper but ignores the actual file system state. That's not a logic bug—it's a mismatch between what you assumed and what the OS delivers.

'We spent two weeks chasing a phantom skip. Turned out the file was there—the script just didn't have permission to open it.'

— Senior DevOps engineer, post-mortem notes for a payment pipeline outage

The frustration compounds because these scenarios feel like edge cases until they hit production daily. A network share that works fine at 2 PM might fail at 6 PM when the backup agent runs and holds files open. Your Monday batch succeeds; Tuesday's identical run skips three files. The lack of consistency is the trap—it convinces you the script is fine most of the time, so the rare skip must be an external glitch. That thinking kills budgets. Fix the root checks first; stop guessing.

2. Check #1: Is the File Being Used by Another Process?

The Lock That Blocks Your Script

Your automation script glides through a directory, hits a critical CSV file, and just… skips it. No crash. No error. Just silence. Most of the time the culprit is a file lock — another process has a grip on that file, and your script can’t pry it open for reading. I have watched teams burn three hours debugging a pipeline when the real problem was a stale Excel window holding the file hostage. That hurts.

The tricky bit is that file locking behaves completely differently across operating systems. On Windows, the locking model is aggressive by design — the OS will happily deny read access to files a program has opened with exclusive write permissions. A user who double-clicks a CSV, glances at it, then minimizes the window? That file is effectively dead to your script until the user closes the app. Linux and macOS are more permissive: you can usually read a file that another process is writing to, though you might get a torn read — half old data, half new. That's its own kind of silent failure. So which world are you in?

‘The file showed up in my directory listing. The script just refused to touch it. Windows Defender was scanning it.’ — Incident postmortem, mid-market SaaS team

— An actual Slack snippet from a team that spent a sprint on the wrong root cause.

Spotting the Lock: lsof, Handle, and a GUI Lifesaver

If you're on Linux or macOS, lsof is your first weapon. Run lsof /path/to/your/file.csv and it lists every process touching that file. The second column is the PID — kill the process or wait for it to finish. Simple? Yes. But most people forget to check child processes spawned by their own automation framework. I once found that Airflow’s scheduler held a read-lock on a config file because a previous DAG run had not released its handle. Wrong order entirely.

Windows users need the Sysinternals suite. Download Handle.exe from Microsoft or open Process Explorer, press Ctrl+F, and type the filename. The tool shows you which process (PID, full path, handle type) has the lock. The catch is that Explorer.exe itself holds locks on files in open folders — close File Explorer windows first or you will chase ghosts. Quick reality check: run handle -a -u filename from an admin command prompt to see the user context. This matters when your script runs as SYSTEM but the file is locked by a user session.

What about tools built into your script? Python’s os.access() is famously unreliable for lock detection — it checks permissions, not actual holding processes. Don't trust it. Instead, use psutil to inspect open file handles on the target PID, or wrap your file-open call in a try-except that catches PermissionError and logs the exception immediately. Most teams skip this: they swallow the error and move to the next file. That's how a single lock collapses a thousand-file batch without a peep.

One last pitfall: some file watchers (like inotify on Linux) report a file as “available” the moment its inode changes, before the writing process releases its lock. Your script opens the file, reads garbage, and marks it as processed. Not yet. Always verify handle release by attempting an exclusive open in a retry loop with a small sleep — three attempts, 200ms apart, then log and skip. This adds 600ms worst-case per contentious file, which beats debugging a corrupted dataset at 2 AM.

3. Check #2: Does Your File Pattern Actually Match?

Wildcard pitfalls: The silent excluders

You wrote *.log and walked away, confident. Meanwhile your script ignored myApp.LOG because the filesystem is case-sensitive on Linux. Ouch. I have seen a five-hour ETL pipeline fail because someone used *.txt and the source system wrote .TXT. Hidden files? They bite too—.config.yaml won’t match *.yaml unless you explicitly include dotfiles. And extension double-ups—report.tar.gz doesn’t match *.tar.gz? Actually it does, but report.final.tar.gz might if your glob only grabs one level of wildcard. The catch is that **/*.csv works in Python’s pathlib but fails in older glob.glob() calls. Test your pattern on a dry run first—print the matched list, not the execution result. Quick reality check: add hidden=True or prepend .* where appropriate, but beware of grabbing .. directory entries on accident.

Regex gotchas: When special characters ambush you

Regex is a scalpel—and just as easy to cut yourself with. A file named data(2024).csv gets skipped because parentheses are group operators, not literal characters. I fixed a deployment script once where report_* worked in shell glob but the Python re.search() expected report_.*. That mismatch cost us three hours of false-negative alerts. Escaping is the obvious fix, but most teams skip this: they test against one file, not a production directory with dashes, plus signs, and spaces. The period . matches any character, so config.ini might slurp configXini if you forgot to escape it. re.escape() is your friend—use it on the filename portion, not the pattern wildcard. One more trap: anchoring. ^data.*\.csv$ is strict. data.*\.csv without anchors will match backup_data_v2.csv when you only wanted data_v2.csv.

Your pattern matched perfectly in the test runner. The production server had different locale settings. Your regex broke before the first file.

— A field engineer, after a root-cause postmortem

That sounds fine until you run a script at 3 AM on a server where re.UNICODE handles accented characters differently. Same pattern, different environment, silent exclusion. The fix? Write a small test harness that compares expected file lists against actual matches—commit it to your repo. Not a unit test with three files; a fixture with edge cases: hidden files, uppercase extensions, parenthesized names, and files with trailing spaces (yes, those exist). Run it before deployment. A moment of verification beats a night of debugging.

4. Check #3: Are You Swallowing Errors Silently?

The Danger of Overly Broad Try-Catch Blocks

You wrapped your file processing logic in a try-catch block. Good instinct—until that block catches everything from a missing semicolon in your regex to a full disk. I have seen scripts run happily for weeks, silently skipping a critical invoice file every Sunday because a network timeout was swallowed inside a generic Exception ex. The script reported success. The file rotted in a temp folder. The catch is this: Python’s bare except:, JavaScript’s .catch(e) without inspection, or Bash’s 2>/dev/null all turn legitimate failures into invisible ghosts. You get a 100% pass rate and zero confidence.

Add Logging Granularity: Warnings vs. Errors

The fix isn’t to remove error handling—it’s to make it honest. Most teams I work with start by categorizing every exception: a missing optional config file gets a warning and continues; a locked database connection gets an error that halts the chain. But the real trap is the unhandled edge case that falls into a broad catch-all at the end. That’s where you log a generic message like “Processing failed for file X” and move on—no stack trace, no context about which operation blew up. Wrong order.

Instead, attach a unique error code per logical step. When the file-open call fails, log ERR_OPEN_022. When the parse step chokes on a malformed row, log ERR_PARSE_037. Then your monitoring dashboard can tell you immediately: “We had 14 parse errors, not open errors.” Quick reality check—that granularity saved a client of mine three hours of digging through a 20,000-line log file last quarter. They had been treating a corrupted CSV as a “skipped file” for two weeks.

“Error handling is like insurance: you only notice the gaps when something expensive burns down.”

— paraphrased from a production incident post-mortem I wrote in 2023

The trade-off is real: adding granular logging bloats your codebase by maybe 30 lines per module. But that overhead pays for itself the first time a silent skip costs you a compliance deadline. One pattern I see fail repeatedly is developers logging errors but never checking the log output. You need a separate monitoring step—a simple cron job that greps for ERR_ patterns and alerts Slack if any count exceeds zero. Without that, you’re just writing notes that nobody reads. The last pitfall: don’t log secrets or PII in those error messages. That fix creates its own mess.

Most automation scripts fail not because the file is missing, but because the error handler misdiagnosed the problem. After you’ve verified file locks and patterns, open your error log. Look for the line that says “completed successfully” immediately before a file was silently dropped. That’s your smoking gun—fix the catch block, not the file path.

5. When These Checks Don't Work (Edge Cases)

When the File Vanishes Into a Network Gap

You run check #1—no lock. Check #2—pattern matches perfectly. Check #3—no silent swallows. The file still doesn't get processed. I have watched teams chase this ghost for hours, only to find the culprit hiding in network topology. Your script polls a remote directory every thirty seconds. The file arrives at t+28 seconds, your script fires at t+30, and nothing is there. That hurts.

The real problem is stateless retry logic. Most automation tools treat a missing file as a permanent failure, not a transient hiccup. They write "file not found" to a log and move on. But the file was in transit—stuck in a TCP buffer, delayed by a router hop, or sitting in an NFS write cache that hasn't flushed yet. A single retry with a one-second backoff catches maybe 40% of these cases. Three retries with exponential backoff catches nearly everything. The catch is—every retry adds latency, and latency angers downstream consumers. Trade-off: reliability versus speed.

Quick reality check—are you mounting network drives with the `hard` or `soft` option? `soft` mounts fail fast with an error, which your error handler probably discards as "no file." `hard` mounts block until the server responds, which masks transient gaps but hangs your script dead when the mount truly dies. Neither is correct; both are dangerous. I fixed one deployment by switching to a message queue that acknowledged file readiness only after the data was fully flushed to disk and the file handle was released. Different architecture, same goal: stop pretending the network is invisible.

Permission Inheritance That Lies to You

Check #1 says the file isn't locked. Check #2 confirms the pattern. Check #3 shows no errors. Yet the script reads an empty list. This is where permission inheritance gets sneaky—your script runs as `automation_user`, the file lives under `/shared/staging/`, and the group `batch_workers` has read access. Looks fine. But the directory itself? `/shared/` has a parent ACL that explicitly denies `batch_workers` the `list` permission. The file is readable—once you know its name. Your script can't discover it because `ls` and `glob` silently return nothing.

Most teams skip this: testing directory traversal rights separately from file read rights. They test `cat /shared/staging/report.csv` and it works, so they assume the entire pipeline works. But `os.listdir()` or `FindFirstFile` uses different NTFS permissions than open-by-handle. On Linux, the `x` (execute) bit on a directory controls traversal—not `r`. Wrong order? The file exists but the script can never see it. One client spent three days debugging a backup script that ran fine as root but failed under a service account. The service account had read access to every file. It lacked execute on the parent directory. Three days.

'I checked the file permissions three times. I never checked the folder above it.' — a friend who now adds an ACL audit script to every deployment.

— A clinical nurse, infusion therapy unit

— The folder's ACL was inherited from a security policy that locked down `/finance/` for compliance. Nobody remembered the inheritance was blocking traversal.

What actually works: run your script with a one-liner that attempts to list the directory and open a known file in that directory. Compare the two results. If listing fails but opening succeeds, you have a traversal permission hole—the file is there, your script can touch it by full path, but pattern-based discovery is blind. And pattern-based discovery is what most automation uses. Fix the ACL or hardcode the file path. Either way, stop trusting inheritance.

6. Limits of the '3 Checks' Approach

When to escalate to system monitoring

The three checks I laid out are your first-aid kit—not a hospital. They catch the obvious: a locked file, a mismatched glob, swallowed exceptions. But what happens when your script runs perfectly on every test file and still skips the same production CSV every Tuesday at 3:47 AM? That stinks of a transient condition your code never sees. The file exists, the pattern matches, nothing throws—yet the pipeline silently drops it. I have watched teams burn three days blaming their own logic when the real culprit was a network-mounted volume that briefly unmounts during storage snapshots. Your script doesn't log a failure because from its perspective, the directory simply had one fewer entry. Normal error handling can't detect what never goes wrong.

This is where you need system-level observability, not script-level debugging. File access audit logs, disk I/O latency metrics, and process start-time correlation can reveal patterns invisible inside your Python or Bash script. If the skipped file appears healthy when you SSH in and check manually, stop guessing—instrument the server, not the code. A five-minute `strace` session or a CloudWatch metric on `ReadOps` will tell you more than four hours of adding debug prints. The painful truth: once a file is missed and your three checks pass, you have left the domain of automation debugging and entered infrastructure forensics.

Complex dependencies and race conditions

The three-checks model assumes a static snapshot—file exists, file is free, pattern matches. Real automation lives in a river. Consider a script that processes log files rotated by a daemon at midnight. You check: file is there, no lock, matches `*.log`. Good. But what if the rotation happens between your check and your open? The file is renamed mid-stream. Your script sees the old path vanish and either creates an empty new file or errors out. The checks passed, the timeline shifted. That's a race condition, and no combination of `os.path.exists` + `try/except` will cure it.

Worse are dependency chains where multiple files must arrive in a specific order. I once saw an ETL that checked for `sales.csv` and `inventory.csv` independently—both passed, both were unlocked—yet it merged them in the wrong order because `sales.csv` had a delayed header write. The individual checks were correct; the relationship between them was not. When your automation depends on temporal ordering across files, you need more than file-level inspections—you need transaction markers or at least a sequence token. The three checks treat each file as an island. Many production failures are archipelago problems.

“A script that checks each file alone is debugging the trees. The fire is in the timing between them.”

— excerpt from a postmortem I wrote after a batch job corrupted 12,000 records

If you suspect race conditions, escalate to queue-based architectures or idempotency keys—no amount of `sleep(1)` hackery will stabilize a race you can't reproduce locally. The limits of these three checks are not a failure of the method; they're a signal that your problem has outgrown file-level logic. When that happens, dial in a monitoring platform, slap a correlation ID on every file batch, and write a test that deliberately races the scheduler. The checks buy you time. They don't buy you domain mastery.

7. Frequently Asked Questions

How to test file access without modifying the script

Stop touching production code just to check if a file is locked. Use lsof on Linux or Handle.exe on Windows — they show which PID holds your file open. Run lsof /path/to/critical.csv before the automation fires; if the file appears, something else grabbed it first. I once watched a backup daemon lock a nightly report for exactly 300 milliseconds — the script ran in that window and skipped it. Quick reality check — you can also test with flock in a one-liner without touching your main automation: flock -n /tmp/test.lock true && echo 'available' || echo 'locked'. If your script uses Python's os.access(), remember it checks permissions, not locks — that distinction burns people constantly. Wrong assumption: a readable file is always writable or openable.

What to log for forensics

Bad logs are worse than no logs. They whisper "file not found" when really the process died reading it. Log the absolute path at read-time, not at startup: paths change between initialization and execution. Also log the return code of every file operation — Python's .read() might succeed but return zero bytes. That hurts. I debugged a three-month-old billing skip because the log showed "processed OK" — it opened the file but read 0 rows after a filesystem race. Add a heartbeat: print "attempting open at $TIMESTAMP" and "close at $TIMESTAMP" — the gap tells you if the file was held or if the script itself froze. One concrete trick — log the inode number alongside the path. If someone renames the file mid-execution, the inode stays the same while the path changes. The discrepancy alone points to a move, not a deletion.

“I added lock logging once. Found out my own cron job opened the file twice — same script, parallel run.”

— Devops engineer on a postmortem thread, 2024

Share this article:

Comments (0)

No comments yet. Be the first to comment!