0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ヘッドレスEdgeのscreenshotがexit 0でもPNGが無い問題

0
Last updated at Posted at 2026-07-21

はじめに

subprocessで外部コマンドを叩いて画像やファイルを作っていると、たまに妙なことが起きます。
コマンドは成功(exit 0)を返しているのに、直後に確認すると肝心のファイルが無い。しかも毎回ではなく、間欠的に。

私はヘッドレスEdgeでスクリーンショットを撮る処理で、まさにこれにハマりました。
原因の切り分けを2回外して、最終的に「ファイルの出現をポーリングで待つ」というシンプルな形に落ち着いた——その過程の記録です。

こんな人に向けて書いています

  • subprocessなどで外部コマンドを呼び、ファイルを生成している人
  • 「コマンドは成功しているのに出力ファイルが無い」で困っている人
  • 間欠的にしか再現しない不具合の切り分け方を知りたい人

この記事でわかること

  • 「exit 0なのにファイルが無い」がどういう条件で起こるのか
  • 同じ原理で踏みやすい他の場面(動画書き出し・PDF生成・ダウンロードなど)
  • 終了コードではなく「ファイルの出現」で成否を判定する実装(実際のコード付き)
  • 切り分けを2回外した過程と、そこから得た教訓

前提

  • Python(subprocess.run())とヘッドレスEdge(--headless=new --screenshot)を使っています
  • ただし考え方自体は、他の言語・他の外部コマンドでも同じです

背景:何を作っていたか

mediagenという投稿画像生成ツールを個人で作っていて、Pillowで足りない複雑なレイアウト(グラデ・mono見出し・日本語)をHTML+Edgeヘッドレスに逃がす設計にしています。
その実装中に踏んだ不具合です。

こういう時に起きます(いつ・他の場面・困りポイント)

この不具合、実はEdge特有の話ではありません。
「別のプログラムを呼んでファイルを作らせて、その直後に『できた?』と確認する」——この形なら、いつでも起こりえます。

同じ空振りは、たとえばこんな場面でも起こりえます。

  • ffmpegで動画や画像を書き出す
  • 別ツールでPDFやサムネイルを生成する
  • 別プロセスにファイルをダウンロードさせる

いずれも「別のプログラムが、自分の裏でファイルを書く」処理です。同じ原理で、同じ罠を踏みかねません。

そして、この手の不具合が厄介なのは次の3点です。

  • エラーが出ない:コマンドは成功(exit 0)を返すので、「失敗した」と気づけない
  • 間欠的:毎回ではなくたまに起きるので、再現させにくい
  • 成功扱いで先に進む:そのまま次の処理に進んでしまい、後工程で「ファイルが無い」と別の顔で問題が出る

前提知識

  • subprocess.run() は指定したコマンドの終了を待ってから返る(result.returncode が 0 なら正常終了)。
  • Edgeの --headless=new --screenshot=<path> は、指定URLを描画してPNGを書き出す。
  • ここで前提にしたいのは「exit 0=出力ファイルが存在する」とは限らない、という一点です。

手順:切り分けを2回外した経緯

一番気持ち悪かったのは「exit 0 なのにファイルが無い」でした。
成功を返しているので、そもそも失敗として扱っていいのかから迷います。

最初に疑ったのは、Edgeが常駐して競合しているという説でした。
でも実測すると、msedgeプロセスは0個でした。
プロセス一覧が0個と出た時点で常駐競合説は崩れているのに、まだ環境のせいにしていました(1つ目の外し)。

次に疑ったのは、一時プロファイルのlockfile競合です。
プロファイルを専用に分離したら改善したように見えたのですが、また再発しました(2つ目の外し)。

転機は、失敗した直後のディレクトリを後から見たときです。
プロセス終了の直後にはPNGが無かったのに、少し後で見ると正常なPNG(約151KB)が出現していました。
ここで「環境のせいではなく、終了直後にはまだ書き込みが見えていないだけかもしれない」と腑に落ちました。

つまり subprocess.run() の直後に is_file() を見るのが早すぎて、空振りしていたわけです。

ひとつ補足しておくと、厳密な内部原因(Chromium側の書き込み完了タイミングなど)まで追い切れてはいません。
なので、ここでは「終了直後にはファイルが見えず、後から出現するタイミング問題」として扱います。

コード例

対処は、プロセス終了後にファイルの出現をポーリングで待ち、出なければ再試行する形にしました。
mediagenの edge_screenshot() はこうなっています(逐語引用)。

def edge_screenshot(command: list[str], png_path: Path, label: str, attempts: int = 3) -> None:
    """--headless=new は子プロセスがPNGを書き終える前に親が終了するため、
    終了後にファイル出現をポーリングし、それでも出なければ再試行する。"""
    detail = ""
    for _ in range(attempts):
        if png_path.exists():
            png_path.unlink()
        result = subprocess.run(command, capture_output=True, text=True, timeout=45)
        for _ in range(25):
            if png_path.is_file() and png_path.stat().st_size > 0:
                return
            time.sleep(0.2)
        detail = (result.stderr or result.stdout).strip()
    raise OSError(f"EdgeのPNG出力に失敗しました({label}{attempts}回試行): {detail}")

ポイントを説明用に整理すると、次のようになっています。

  • 内側の for _ in range(25)time.sleep(0.2) を回すので、1回の試行につき最大5秒ファイルの出現を待つ。
  • 待つ条件は png_path.is_file() だけでなく png_path.stat().st_size > 0 も見る(0バイトのファイルを掴まないため)。
  • 出なければ外側の attempts(既定3回)で再試行する。試行前に png_path.unlink() で前回の残骸を消す。
  • 全部ダメなら OSError を投げる。

呼び出し側では、実際に edge_screenshot(command, png_path, f"size={media}") の形で使っています。
carousel側(render_carousel_html)にも同じ関数を通して同じ待機を効かせています。

なぜ「終了コードを見て終わり」にしなかったかというと、まさに今回、exit 0 が出力の存在を保証してくれなかったからです。
成否は「終了コード」ではなく「実際にファイルが出たか」で判定する、というのがこの関数の考え方です。

もう一点、一時ディレクトリ側では tempfile.TemporaryDirectory(prefix="mediagen-titlecard-", ignore_cleanup_errors=True) のように ignore_cleanup_errors=True を付けています。
プロファイルの後片付けでたまに失敗しても、生成そのものは止めたくないためです。

この形にしてから、連続実行でexit 0の安定を確認できました。

まとめ

外部プロセスで生成したファイルは、プロセスの終了と「ファイルが見える」が同時とは限りません。
今回はそこを取り違えて、常駐競合・lockfile競合と、環境のせいにする切り分けを2回外しました。

教訓としては、0件・空振りを見たときに「無い」と決める前に、「まだ測れていないだけ」を疑うことでした。
失敗フォルダに後からPNGが出ていたのを見るまで、自分は原因を環境に押し付け続けていました。

対処自体は、終了コードではなくファイルの出現で成否を判定し、少し待ってから再試行する、というシンプルなものです。
内部の詳しいタイミングまでは追い切れていませんが、まずは動く形に落とせたので、同じ「exit 0 なのにファイルが無い」に困っている人の手がかりになればと思います。


個人でAIツールや業務自動化を作っているMiyokiといいます。
今回のmediagenのように、自分の作業を楽にするための小さなツールを作っては、その開発ログをZennとQiitaに書いています。

作ったものやポートフォリオはこちらです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?