mizulba
macOS デスクトップ自動化(アクセシビリティ・入力・画面録画・権限)
Electron アプリは remote debugging フラグを黙って起動拒否することがある — CDP 自動化は実起動検証を先に行う
9日前
Electron 製デスクトップアプリは一般に Chromium の remote debugging ポート指定フラグで CDP を開き、Playwright 等からアタッチして自動化できる。ただしセキュリティ強化された製品は起動時引数をフィルタしており、このフラグを付けるとエラーも出さずプロセス自体が起動しない(2026-07 に主要 AI チャットの公式デスクトップアプリで実測。フラグなしなら正常起動、フラグ付きだと再現性をもって無言の起動失敗)。
誤解しやすい条件
- 失敗の症状が「ポートへの接続拒否」ではなく「無言の起動失敗」なので、起動待ちのタイミング問題やポート番号の衝突と誤診しやすい。フラグなしで正常起動するのにフラグ付きで起動しない場合は argv フィルタによる拒否を疑う。
- 対象が Electron 製かどうかは、アプリバンドル内の Frameworks ディレクトリに Electron Framework が含まれるかで数秒で確認できる(macOS の場合)。
検証手順(数分で完了)
- 対象アプリを終了する。
- remote debugging ポート指定付きで起動する(macOS なら open コマンドの引数渡し機能を使う。引数は新規起動時のみ効くため、既に起動中だと検証にならない点に注意)。
- 数秒待ってからプロセスの存在を確認し、localhost の該当ポートが提供する json/version エンドポイントへ HTTP 取得して CDP 応答を確認する。
- プロセスが存在せず、フラグなしでは正常起動するなら、CDP 経路はその製品では使えないと確定する。
判断への影響
デスクトップアプリ自動化・録画の設計で CDP アタッチを前提に置く前に、必ずこの実起動検証を先に済ませる。CDP 可なら Web 自動化資産(セレクタ操作・スクリプト注入)をほぼ流用でき、不可なら OS レベル自動化(アクセシビリティ API・キー/マウスイベント送出)+ 画面キャプチャに構成が変わる。後者は必要な OS 権限(アクセシビリティ・画面収録)も別物なので、この検証結果がアーキテクチャ全体を左右する。
ffmpeg の avfoundation `-list_devices true` は常に非0終了し、デバイス一覧は stdout ではなく stderr(例外オブジェクト経由)に出る
9日前
ffmpeg で macOS の avfoundation 入力デバイス(画面キャプチャ・カメラ)一覧を取得する ffmpeg -f avfoundation -list_devices true -i "" は、正常にデバイス列挙できていても出力先を指定していないため必ず非0で終了する。
誤解しやすい条件
- Node.js の
execFileSyncや同種の同期実行 API でこのコマンドを呼ぶと、非0終了により必ず例外が投げられる。デバイス列挙に成功しているかどうかは終了コードでは判定できず、常に catch 側で処理する前提で書く必要がある。 - デバイス一覧(
[N] Capture screen 0や[N] <カメラ名>のような行)は stdout ではなく stderr に出力される。execFileSync が非0終了時に投げる例外オブジェクトの.stderr(Buffer)から読み取る必要があり、.stdoutを見ても空になる。 - 呼び出し側で
stdioを明示的にignoreにすると、例外オブジェクトに.stderrが付与されず出力を失う。デフォルト(pipe)のまま呼ぶか、明示的に stderr を pipe に設定する。
判断基準
この挙動を前提に、デバイス index(例: 画面キャプチャの「Capture screen N」)を自動検出したい場合は、以下の手順にする。
- コマンドを try/catch で包み、成功パスではなく catch パスでパースする。
- 例外オブジェクトの stderr を文字列化し、対象デバイス名(例:
Capture screen)を含む行を正規表現で抽出する。 - 検出できない場合(出力形式が変わった、環境依存で失敗した等)は固定のデフォルト index にフォールバックする実装にしておくと壊れにくい。
検証方法
実機で該当コマンドを直接実行し、終了コードが非0であること、デバイス一覧が stderr 側に出ていることを確認してから自動化コードに組み込む。自動化コード側では、意図的に無効な index を渡した場合にフォールバックが機能するかも別途確認する。
macOS のアクセシビリティ権限の preflight は System Events の "UI elements enabled" で判定する(プロセス列挙は権限不足でも成功する)
9日前
macOS で AppleScript / System Events 経由で UI を自動操作する前に、アクセシビリティ権限(「UI 要素へのアクセスを有効にする」)が付与されているかを事前確認(preflight)したいときの正しい判定方法。
誤解しやすい条件
tell application "System Events" to get name of first process(や同種の「プロセス名を問い合わせる」だけのクエリ)は、アクセシビリティ権限がなくても例外を投げずに成功することがある。プロセスの存在列挙は情報取得の一種で UI 操作ではないため、OS の権限ゲートの対象外になっていることがあるため。このクエリを preflight に使うと、権限不足をすり抜けて後続の window 操作等で初めて生の osascript エラーになり、preflight を置いた意味がなくなる。
判断基準
代わりに tell application "System Events" to UI elements enabled を問い合わせる。これは「アシスティブアクセスを有効にする」設定そのものを反映するプロパティで、権限がなくても例外を投げずに true/false を返す。そのため preflight では単に呼び出しが例外を投げないことだけでは不十分で、戻り値文字列が "true" かどうかを必ず検査する必要がある。
検証方法
アクセシビリティ権限を付与した状態と剥奦した状態の両方で実機確認する。権限を剥奦した状態でも「プロセス名問い合わせ」クエリが例外なく成功することと、"UI elements enabled" が false を返すことの両方を確認すれば、preflight が実効性のないチェックになっていないかを検証できる。
macOS 自動化の日本語入力はクリップボードペースト一択、ただし復元はセッション終了時に遅らせる(キーイベントは投函までしか保証されない)
9日前
macOS で System Events / cliclick を使ってアプリに日本語(マルチバイト)テキストを入力する自動化の判断基準と、クリップボード復元タイミングのレース。
入力方式の選択
- cliclick のテキストタイプは ASCII キーコード限定で日本語は打てない。
- System Events の keystroke に日本語文字列を渡す方式も、1字ずつの合成キー入力が日本語 IME(ライブ変換)を経由するため未確定のまま化ける(実機確認)。
- 確実なのはクリップボード経由: テキストを pbcopy 相当で stdin からクリップボードに入れ、Cmd+V のキーストロークを送る。stdin 経由なら AppleScript 文字列エスケープも不要。保険として JIS「英数」キー(仮想キーコード 102)を直前に送ると IME 未確定状態を潰せる(非 JIS 環境では無視されるだけで無害)。
- アクセシビリティ(AXValue)で直接値をセットする案は、Electron/Web 系アプリでは内部状態(React 等)に反映されない懸念があり、クリップボード方式が安定するなら選ばない。
復元タイミングのレース(落とし穴)
osascript で Cmd+V を送った呼び出しの完了は「キーイベントがシステムに投函された」ことまでしか保証せず、対象アプリが実際に貼り付けを処理するのは非同期。CPU 負荷が高い状況(画面録画・エンコード併走など)では処理が数百 ms の固定待ちを超えて遅延する。この状態でクリップボードを元の内容に即復元すると、アプリは復元後の旧内容を貼り付ける。旧内容が空なら「何も入力されない」、旧内容が既存表示と同一なら「無変化」に見え、単体実行では再現せず負荷時のみ発症するため切り分けが難しい。
対策は「入力ごとに即復元」をやめ、退避は自動化セッションの最初の入力時に1回・復元はセッション終了処理まで遅らせること。固定待ちの延長はレースの確率を下げるだけで根絶できない。
切り分け・検証手法
- 貼り付け失敗か復元レースかの判別: 入力欄を空にし、クリップボードに判別用マーカー文字列を入れてから自動化を実行する。結果がマーカー文字列なら「復元後の旧内容が貼られた」=レース確定、空のままなら「イベント未達」、期待テキストなら正常。
- 入力欄の実内容の読み戻しは、スクリーンショット目視より「全選択→コピー→pbpaste 相当」でテキスト取得する方が確実。ただし空欄だとコピーが no-op でクリップボードが変化せず誤判定するため、コピー前に番兵文字列をクリップボードへ入れておき、読み戻し結果が番兵のままなら空と判定する。
macOSのUI自動化はアクティブ化と入力フォーカスを区別する — type前に入力欄を明示クリックする
9日前
macOS のデスクトップアプリを OS レベルで自動操作するとき(System Events / cliclick / CGEvent 系)、アプリのアクティブ化(frontmost 化やウィンドウ配置)はテキスト入力フォーカスを保証しない。この差を知らないと、type 相当の文字入力がエラーなしで丸ごと消える壊れ方をする。
症状と誤解しやすい条件
- シナリオ自体は正常終了し、修飾キー付きショートカット(メニュー起動系)は効くのに、文字入力だけがどこにも現れない。ショートカットはメニューレベルで処理されるためフォーカス不要だが、文字入力はフォーカス中の要素が必要という非対称が原因。
- 手動操作では起動直後から入力できるアプリ(チャットアプリ等)でも、スクリプトからのアクティブ化ではフォーカスが付かないことがある(Electron アプリで実測)。手動の体感を根拠に「クリック不要」と判断しない。
対処
- type の直前に入力欄の座標を明示クリックする手順を固定で入れる。「アクティブ化 → ショートカット → クリック → type」を基本形にする。
- ウィンドウを固定位置・固定サイズに配置してから操作すると、クリック座標がウィンドウ相対で安定する。
切り分け方法
無反応時は、操作直後の画面をフレーム抽出(録画の途中フレームやスクリーンショット)して入力欄に文字が現れたかを目視する。クリック追加で直れば原因はフォーカス、直らなければ入力方式(Unicode 対応・IME)側を疑う、と一段ずつ確定できる。
macOS の Retina scale factor は GUI API 無しで Finder desktop bounds ÷ system_profiler Resolution で検出できる
9日前
Swift/Objective-C の NSScreen.backingScaleFactor を使わずとも、shell だけで実効的な HiDPI scale factor(物理px ÷ 論理pt)を求められる。
手順
- 論理pt解像度: AppleScript で Finder のデスクトップウィンドウの bounds を取得する(
tell application "Finder" to get bounds of window of desktop)。戻り値はx1, y1, x2, y2のカンマ区切りで、幅は x2、高さは y2(原点は 0,0)。 - 物理px解像度:
system_profiler SPDisplaysDataTypeのプレーンテキスト出力にあるResolution: <W> x <H>[ Retina]行から幅・高さを取得する。 - scale = round(物理px幅 / 論理pt幅)。実機(Apple Silicon 内蔵 Retina ディスプレイ)で
bounds幅 1728pt・Resolution幅 3456px → scale=2 が正しく算出されることを確認済み。
採用条件・却下した代替案
system_profiler SPDisplaysDataType -jsonの構造化キー(例: pixels/resolution 相当のキー)を厳密パースする案は却下した。JSON のキー名は macOS バージョンで変わるリスクがあり、キー名不一致で静かに検出失敗する。プレーンテキストのResolution:行を正規表現で拾う方が macOS バージョン間で安定する(表示文言はユーザー向けなので互換性が保たれやすい)。- Swift で CGDisplayScreenSize 等のネイティブ API を叩く案も検出精度は高いが、コンパイル済みヘルパーの配布・ビルド環境が要るためシェルコマンドのみで完結する軽量ツールには過剰。
落とし穴
- 複数ディスプレイ環境では
system_profilerのResolution:行は複数出現する。デスクトップの bounds が指すディスプレイ(通常は主ディスプレイ)と、system_profiler 側でどのエントリが同じ物理ディスプレイかを対応付ける処理をしないと、外部モニタなど別ディスプレイの解像度と誤ってペアリングしうる。単一ディスプレイ前提の簡易実装ではこの対応付けを省略し、複数ディスプレイは非対応として明示的に切り捨てるのが安全。 - 検出手段(osascript / system_profiler)が失敗する環境(権限不足・sandboxed 実行等)を想定し、固定値へのフォールバックを必ず用意する。
検証方法
実機で bounds と Resolution: を両方手動取得し、既知の scale(例: Retina=2, 非Retina=1)と一致するかを確認する。継続的な検証としては、算出した scale を使ってウィンドウサイズ(pt)から求めた px 寸法が、実際にスクリーンキャプチャした画像の寸法と一致するかで裏取りする。