春江暮客

春江暮客的个人学习分享网站

Run Commands from Python: Timeouts, Errors, and Log Files

2026-10-08 Technology
Run Commands from Python: Timeouts, Errors, and Log Files

A Python pipeline calls an external tool, the tool fails, and the pipeline continues with an incomplete output file. Or a command waits for input that nobody will provide. Both problems become harder to diagnose when the terminal output is gone.

This tutorial builds a small command runner that stops on errors, applies a timeout, and writes output to a fresh log. The demo uses Python itself as the child process, so you can test every case without installing another tool.

1. Define the runner’s job

This helper runs one trusted executable at a time. It uses an argument list, closes standard input, and sends both output streams to disk. Each run needs a new log filename. It does not retry commands or manage a job queue.

The key API behavior comes from Python’s subprocess.run documentation: check=True raises on a nonzero exit, and timeout kills and waits for the direct child before raising. Process creation can delay the timeout. An argument list handles spaces without manual shell quoting.

2. Download and run a successful command

Use Python 3.10 or newer. No external packages are required. The examples were tested on macOS with Python 3.14.7; the shell commands below assume a POSIX shell such as bash or zsh.

Download run_logged.py into a new working directory, then run:

python3 --version
python3 run_logged.py --help
mkdir subprocess-demo
python3 run_logged.py ok subprocess-demo/ok.log
cat subprocess-demo/ok.log

Expected output from the last two commands:

OK: log=subprocess-demo/ok.log
processed 3 records

Use fresh paths when repeating the examples. Existing logs are deliberately preserved.

3. The complete implementation

"""Run a trusted command with a fresh log and a timeout. Python 3.10+."""
import argparse
import math
from pathlib import Path
import subprocess
import sys


def run_logged(argv, log_path, *, timeout=60):
    """Keep stdout/stderr on disk; propagate launch, exit and timeout errors."""
    if isinstance(argv, (str, bytes)) or not argv:
        raise ValueError("argv must be a nonempty argument sequence")
    if not math.isfinite(timeout) or timeout <= 0:
        raise ValueError("timeout must be a finite positive number")
    # Exclusive creation prevents accidental replacement of an earlier log.
    with Path(log_path).open("xb") as log:
        return subprocess.run(
            argv,
            stdin=subprocess.DEVNULL,
            stdout=log,
            stderr=subprocess.STDOUT,
            shell=False,
            check=True,
            timeout=timeout,
        )


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("mode", choices=["ok", "fail", "timeout"])
    parser.add_argument("log", type=Path, help="new log path; parent must exist")
    args = parser.parse_args()
    programs = {
        "ok": "print('processed 3 records', flush=True)",
        "fail": "import sys; print('invalid input', file=sys.stderr, flush=True); sys.exit(2)",
        "timeout": "import time; print('started', flush=True); time.sleep(30)",
    }
    command = [sys.executable, "-u", "-c", programs[args.mode]]
    try:
        run_logged(command, args.log, timeout=1 if args.mode == "timeout" else 10)
    except subprocess.CalledProcessError as exc:
        print(f"FAILED: exit={exc.returncode}; log={args.log}", file=sys.stderr)
        return 1
    except subprocess.TimeoutExpired:
        print(f"TIMEOUT: log={args.log}", file=sys.stderr)
        return 1
    except (OSError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(f"OK: log={args.log}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

The demo’s -c programs are fixed strings under our control. sys.executable selects the interpreter running the parent script. In your pipeline, replace that command list with the actual executable and its arguments.

Opening the log with xb rejects an existing path before starting the command. Binary mode preserves the child’s output bytes. The parent directory must exist. If launch fails after the log is opened, an empty log can remain; that file alone is not proof that a job ran.

4. Exercise failure and timeout paths

Run these commands one at a time. The two runner calls intentionally return exit status 1, so do not put this demonstration in a script that stops at its first error.

python3 run_logged.py fail subprocess-demo/fail.log
cat subprocess-demo/fail.log
python3 run_logged.py timeout subprocess-demo/timeout.log
cat subprocess-demo/timeout.log

The failed command produces:

FAILED: exit=2; log=subprocess-demo/fail.log
invalid input

The timeout produces:

TIMEOUT: log=subprocess-demo/timeout.log
started

exit=2 is the child’s status; the demo CLI maps both failure and timeout to its own status 1. Importing run_logged directly preserves the original exceptions for your application to handle.

The timeout demo uses unbuffered Python output and flushes its first message. Other programs may buffer their output, leaving a partial or empty log when stopped. Logging to a file also means output consumes disk space: set retention and size policies for long-running jobs.

5. Pass a filename containing spaces

Save the following as count_demo.py beside run_logged.py, in a scratch directory. It creates a tiny worker script and input file, then counts nonempty lines. These fixture files are overwritten if they already exist.

import sys
from pathlib import Path
from run_logged import run_logged

worker = Path("count_records.py")
worker.write_text(
    "import sys\n"
    "from pathlib import Path\n"
    "with Path(sys.argv[1]).open(encoding='utf-8') as handle:\n"
    "    print(sum(1 for line in handle if line.strip()))\n",
    encoding="utf-8",
)
Path("sample records.txt").write_text("alpha\nbeta\ngamma\n", encoding="utf-8")
run_logged(
    [sys.executable, str(worker.resolve()), "sample records.txt"],
    "count.log",
    timeout=10,
)
assert Path("count.log").read_text().strip() == "3"
print("PASS: counted 3 nonempty lines")

Run python3 count_demo.py. It prints PASS: counted 3 nonempty lines. The filename is one list element; adding literal quote characters around it would change the argument.

For a real pipeline, validate the output before recording completion. Pair that check with the atomic JSON save helper when updating a small progress file, and keep an input checksum manifest if you need to identify the exact data used.

6. Check that old logs survive

After the counting example, save this as check_log.py and run python3 check_log.py in the same directory:

from pathlib import Path
from run_logged import run_logged
import sys

path = Path("count.log")
before = path.read_bytes()
try:
    run_logged([sys.executable, "-c", "print('replacement')"], path)
except FileExistsError:
    assert path.read_bytes() == before
    print("PASS: existing log preserved")
else:
    raise AssertionError("Expected FileExistsError")

This verifies that a repeated run cannot silently erase the previous log. For production runs, choose a new run directory or unique log name rather than deleting logs automatically.

7. Troubleshooting and boundaries

Symptom Action
FileExistsError Choose a fresh log path.
FileNotFoundError Check both the executable and the log’s parent directory.
FAILED: exit=... Read the log and check the tool’s documented exit codes.
TIMEOUT Inspect the partial log, check the workload, then choose an appropriate limit.
Tool immediately reports missing input This runner closes stdin; pass input as a file argument or adapt the helper for stdin.
Log looks scrambled Open it using the child program’s output encoding; bytes are preserved.

The runner does not cap log size, roll back partial result files, or guarantee termination of descendants spawned by the direct child. Tools that launch worker processes need a process-tree strategy appropriate to the operating system or a job supervisor. A zero exit status still needs an application-level output check.

Keep commands and options trusted. shell=False is not a sandbox, and a tool can still interpret an argument as an option. Windows batch files also need special care; see Python’s subprocess security notes. This example targets ordinary executables and Python scripts.

8. Summary

Give each command a fresh log, a deliberate timeout, and an explicit error path. Test failure cases before inserting the runner into a batch pipeline. Treat process success and valid output as separate checks before saving progress.

标签

1024 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena copy cpu monitoring cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download economics electron encoding esm esm2 esm3 esmc esmfold2 fable faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print probability protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode usdt utf-8 uv vhhbert vite wallet webp wordcloud wordpress workflow yaml zipfile zoneinfo 迅雷解析

友情链接

其它