Maya `Cannot find procedure "look"` への対処方法

皆様ごきげんよう。全国一千万のmaya界では7.31.19で話題フットー中であるがいかがお過ごしだろうか。閑話休題、つい先日筆者は maya で作業中、特定のシーンデータを開きアウトライナを操作すると `Cannot find procedure "look"` というエラーが発生する現象、 https://qiita.com/ti_ni_ta/items/bdbf30d0236addb267a2 、 https://qiita.com/mono-g/items/457b1c80147daa059eb2 そして https://forums.autodesk.com/t5/maya-forum/error-line-1-cannot-find-procedure-quot-look-quot/m-p/9035867/highlight/true https://forums.autodesk.com/t5/maya-forum/error-cannot-find-procedure-quot-dcf-updateviewportlist-quot/td-p/8342659?fbclid=IwAR3IhCeCqzZvEREmxo5eA7ECQu9n82MEN_vqCFTmdySwNNbsrYREdDcv_QA にかかれている現象に遭遇した。だが上記のサイトの手法では解決には至らなかった（というかむしろ危険で不完全な解決方法が書かれている）ため、この記事で対処法について紹介しよう。

## なにがおきるのか
詳しくはリンクを参照していただきたいが、***mayaシーンファイルを媒介とした感染性のエラー***である。表題のエラーは `アウトライナ` で要素を選択した際に逐一エラーとなるコールバックが発火している。この記事にたどり着いた方には言うまでもないが非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に非常に煩わしい。

具体的にはシーンファイル中に UI設定が記述されており、そのなかにアウトライナのコールバックが設定されている。このUI設定がシーンを開いた際、ローカルのmayaの設定に転写され、以降そのローカルマシンで保存するシーンファイルにも記述される。この設定がエラーを引き起こす。もうなんか非常にアレ。


## 対応
『シーンに記述されたコールバックがエラーを起こすのであれば、そのコールバックを上書きする、さらにはそのような設定を保存しない、ことが免疫として機能する」と考えるのは妥当だ。実際 `modelEditor` については機能するのだが、実は `OutlinerEditor` の `selectCommand` コールバックにかぎっては前掲のリンクの対応では不十分である。ここではそれへの対処を記述する。

## コールバックの空文字での上書きが不十分な理由と対応
空文字を渡してもコールバックが削除されず、空文字を実行しようと？する。設定したそのセッションではそれで充分なのであるが、なぜか次セッションでは mel的にはエラーとなる。興味がある方は workspace の保存を有効にした状態で `cmds.outlinerEditor("outlinerPanel1", e=True, selectCommand="")` を実行し何が起きるかを試してみるとよい。


なので空文字の設定ではなく `outlinerEditor` 自体消し飛ばすことにする。以下コードを示す

```py
def eliminate_outliner_callback(client_data):

    all_panels = cmds.getPanel(type="outlinerPanel") or []
    for panel in all_panels:

        sc = cmds.outlinerEditor(panel, q=True, selectCommand=True)
        if sc is not None:  # if there are selectCommand set...

            print("KILL INFECTED outliner!!!")

            cmds.outlinerPanel(panel, e=True, unParent=True)  # remove infected panel
            cmds.file(uiConfiguration=False)  # mark as do not save ui info within scene file

            if (
                    cmds.optionVar(exists="useScenePanelConfig") and
                    cmds.optionVar(q="useScenePanelConfig") == 0
            ):
                mel.eval("$gOutlinerPanelNeedsInit = 1;")  # flag to restore later

    mel.eval("initOutlinerPanel();")  # restore outliner if flagged
```

のようなコマンドを

```py
    om.MSceneMessage.addCallback(
        om.MSceneMessage.kAfterOpen,
        eliminate_outliner_callback
    )
```
シーンオープンに引っ掛けるなどしておくとよいだろう。上記のコードでは、シーンを開いた際コールバックの設定された **INFECTED なoutliner**がある場合はいったん閉じ、必要に応じてアウトライナの再初期化を行っている。必要に応じて、というのはつまり、表題のエラーがおきるトリガが各ローカルの設定に依存しているからだ。無害であるのであればそれ以上の感染拡大を ` cmds.file(uiConfiguration=False)` で防ぎつつ素通ししている。


## おわりに
この記事では対処療法をしめしたが、そもそもこのようなことが起きないようなシステムを望みたい。シーン中にこのようなコールバックの記述および読み取りを含むのは明らかに不要だ。また maya ではないがしばらく前にはシーンファイルを媒介とした攻撃意図をもったファイルの存在が話題になった。各ベンダには安全な設計を願いたい。

## おまけ
`CgAbBlastPanelOptChangeCallback` や `DCF_updateViewportList` のエラーが出る場合

```py
def eliminate_model_panel_callback(*args):

    EVIL_METHOD_NAMES = ['DCF_updateViewportList', 'CgAbBlastPanelOptChangeCallback']

    model_panel_label = mel.eval('localizedPanelLabel("ModelPanel")')
    processed = []

    panel_name = cmds.sceneUIReplacement(getNextPanel=('modelPanel', model_panel_label))
    while panel_name and panel_name not in processed:

        editor_changed_cb_text = cmds.modelEditor(panel_name, query=True, editorChanged=True)
        suspected_lines = editor_changed_cb_text.split(';')
        purified_lines = []
        changed = False

        for line in suspected_lines:
            for evil in EVIL_METHOD_NAMES:
                if evil.upper() in line.upper():
                    changed = True
                    break
            else:
                purified_lines.append(line)

        if changed:
            logger.info("kill infected modelPanel %s", panel_name)
            cmds.modelEditor(panel_name, edit=True, editorChanged=';'.join(purified_lines))
            cmds.file(uiConfiguration=False)  # mark as do not save ui info within scene file

        processed.append(panel_name)
        panel_name = cmds.sceneUIReplacement(getNextPanel=('modelPanel', model_panel_label))
```