codex/unlink-open-file-lifetime.py

unlink-open-file-lifetime.py
"""Draw abstract link/open-reference lifetimes; Python 3.14 / matplotlib 3.10.7.

Run from _media; write only the matching PNG in the current directory.
The names, references and unlink/close operations are Python-only models.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch


INK = "#203040"
BLUE = "#155d8a"
GREEN = "#186a52"
ORANGE = "#935017"


def stages():
    inode = {"number": 42, "contents": b"hello\n"}
    names = {"a": inode, "b": inode}
    descriptions = [{"inode": inode, "position": 0}]
    result = []

    def capture():
        links, opens = len(names), len(descriptions)
        retained = bool(links or opens)
        result.append({"names": tuple(names), "links": links, "opens": opens,
                       "retained": retained})
        if descriptions:
            assert descriptions[0]["inode"]["contents"] == b"hello\n"
        if not retained:
            inode.clear()  # Reclamation in the model, never a filesystem operation.

    capture()
    del names["a"]
    capture()
    del names["b"]
    capture()
    assert descriptions[0]["inode"]["contents"] == b"hello\n"
    descriptions.clear()
    capture()
    assert [(s["links"], s["opens"]) for s in result] == [(2, 1), (1, 1), (0, 1), (0, 0)]
    assert [s["retained"] for s in result] == [True, True, True, False]
    assert not inode
    return result


def main():
    snapshots = stages()
    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12})
    fig, ax = plt.subplots(figsize=(12, 6.1), dpi=110)
    fig.patch.set_facecolor("white")
    ax.set(xlim=(0, 13), ylim=(0, 6.5))
    ax.axis("off")

    def label(x, y, text, size=12, color=INK, weight="normal"):
        ax.text(x, y, text, ha="center", va="center", fontsize=size,
                color=color, weight=weight, linespacing=1.6)

    def box(x, y, w, h, text, color, fill, size=12):
        ax.add_patch(FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.035",
                                   edgecolor=color, facecolor=fill, linewidth=1.5))
        label(x + w / 2, y + h / 2, text, size=size, color=color)

    label(6.5, 6.08, "Removing names does not close an open file", 21, weight="bold")
    label(6.5, 5.6, "L = hard links • O = live open file descriptions • one descriptor, fd 3", 12)
    xs = (0.25, 3.5, 6.75, 10)
    headings = ("Initial state", "Name a removed", "Last name removed", "Last reference closed")
    for x, s, heading in zip(xs, snapshots, headings):
        middle = x + 1.35
        color = GREEN if s["links"] else ORANGE if s["opens"] else "#596675"
        label(middle, 4.82, heading, 12, weight="bold")
        box(x, 4.06, 2.7, 0.5, f"L = {s['links']}      O = {s['opens']}", color, "#f5f7f9", 14)
        names_text = ", ".join(s["names"]) if s["names"] else "none"
        label(middle, 3.6, f"Names: {names_text}", 13)
        if s["retained"]:
            box(x + 0.1, 2.31, 2.5, 0.9, "Inode 42 retained\n6 bytes: hello + newline", color, "#edf4f1", 11)
            box(x + 0.1, 0.92, 2.5, 0.75, "fd 3 → open description\n→ same file", BLUE, "#edf4fa", 11)
            ax.add_patch(FancyArrowPatch((middle, 1.73), (middle, 2.25),
                                        arrowstyle="-|>", mutation_scale=15,
                                        linewidth=1.5, color=BLUE))
            status = "Still named and readable" if s["links"] else "Unnamed, still readable"
        else:
            box(x + 0.1, 2.31, 2.5, 0.9, "Inode and data blocks\nreclaimed", color, "#f5f7f9", 12)
            label(middle, 1.3, "fd 3 closed\nNo open reference", 12, color)
            status = "Storage becomes reusable"
        label(middle, 0.4, status, 11, color)

    for left, right, operation in zip(xs, xs[1:], ('unlink("a")', 'unlink("b")', "close(3)")):
        # Arrows and labels sit above the cards, away from their state contents.
        start, end = left + 2.4, right + 0.3
        ax.add_patch(FancyArrowPatch((start, 5.16), (end, 5.16), arrowstyle="-|>",
                                    mutation_scale=15, linewidth=1.6, color=INK))
        label((start + end) / 2, 5.37, operation, 10)
    fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white",
                transparent=False)
    plt.close(fig)
    print("Abstract stages: (2,1) → (1,1) → (0,1) → (0,0); retained: yes, yes, yes, no.")


if __name__ == "__main__":
    main()