起動中のMayaに対する実用的な外部操作 (を作りたかった)

## はじめに
はじめまして、赤城 巧です。  
普段はゲーム会社でTA[^1]をやっております。

今回は起動中のMayaへの外部操作について書きました。

[^1]: 以前、パイプラインTAもどき、と自己紹介しましたが、これまでの作業を振り返るとパイプラインの仕事すらありませんでした。正しくは雑用系TAでした。誠に申し訳ございません。


## とりあえず
まずはGoogleで「Maya リモート」で検索します。  

検索するとTAならよく見るサイトがあるので 
以下のページを参考に適当に実装します。

[リモートからの実行 ( Maya API - 08AA )](http://www.not-enough.org/abe/manual/maya-python-aa08/rmc.html)

```userStartup.py
# encoding: utf-8
"""Maya側"""

def open_commandport():
    cmds.commandPort(n=':8573', stp='python')

if __name__ == '__main__':
    open_commandport()

```

```send_command.py
# encoding: utf-8
"""外部操作側"""

import socket

def doIt(command):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        sock.connect(('localhost', 8573))
        sock.send(command)
        return sock.recv(4096)
    except socket.error:
        raise RuntimeError('No exists opened port of Maya. port: %s' % 8573)
    finally:
        sock.close()

```

上記スクリプトを元にして、ツールへ組み込めば完成ですね！！！

## 本当にそうでしょうか？

たとえば、現状のスクリプトを組み込んで実装していると、
以下のようなトラブルが発生します。

* 動かないときがある。
  * 主にMayaを複数起動するユーザーの環境。
    * 既にアプリケーションでポートを開いている場合は、同一のポートは開けない。    
        複数起動したあとにポートを開いているMayaを終了すると、  
ポートを利用できるMayaは存在しなくなり、使えなくなる。

* 連続で操作をすると反応しない。
  * 送信した処理が完了するまで、socketがロックされてしまうため。

とりあえずでは使えますが、ツールに落とし込むのには程遠い状態ですね。


##  どうする？

上記の問題には、以下の方針で対応しました。
* Mayaの複数起動
  * -> コマンド送信に利用するポートを複数用意する
* 外部アプリケーションへの操作の連続送信
  * -> Queue を利用して Maya 側の処理を待ちつつ、連続で送信されたコマンドの実行を担保する

で、出来たのが以下のコードです。

```userStartup.py
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

from maya import cmds

def gen_commandport():
    """コマンドポート候補から開く"""

    START_PORTNUM = 8573
    # 何個候補を挙げるか
    RANGE_NUM = 10
    # コマンドポートの候補
    PORT_RANGE = range(START_PORTNUM, START_PORTNUM + RANGE_NUM)


    opened_port_on_this = [
        int(x[1:]) for x in cmds.commandPort(lp=True, q=True) if x.startswith(':')
    ]

    if any([i in opened_port_on_this for i in PORT_RANGE]):
        return

    for portnum in PORT_RANGE:
        try:
            cmds.commandPort(n=':%s' % portnum, stp='python')
            break
        except RuntimeError:
            continue
    else:
        raise RuntimeError('No generate CommandPort %s' % PORT_RANGE)


if __name__ == "__main__":
    gen_commandport()
```

```send_command.py
# encoding: utf-8
"""Mayaにコマンドを送信する部分。

コマンドを送信するサーバを立ててQueueに入れることで、
短期間に複数回実行しても実行できるようにしている。
"""

from __future__ import unicode_literals

import socket

from Queue import Queue
from threading import Thread

import time
import sys

START_MAYAPORTNUM = 8573
RANGE_NUM = 10
PORT_RANGE = range(START_MAYAPORTNUM, START_MAYAPORTNUM + RANGE_NUM)

# サーバーのポート番号
SERVER_PORT = 5784

# 起動したコマンド処理用サーバのQueueにコマンドがなくなっても待機する秒数
WAIT_SECOND = 20

def is_opened_port(portnumber):
    """ポートが開いているか"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        sock.connect(('localhost', portnumber))
        return True
    except socket.error:
        return False
    finally:
        sock.close()


def send_to_port(portnumber, str_):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        sock.connect(('localhost', portnumber))
        sock.send(str_)
        return sock.recv(4096)
    except socket.error:
        return None
    finally:
        sock.close()


def sender(queue_, portnumber):
    """Queueにたまったコマンドを送信する"""
    while True:
        command = queue_.get(block=True, timeout=WAIT_SECOND)

        send_to_port(portnumber, command)
        queue_.task_done()

def putter(queue_):
    """送られたコマンドをQueueに貯める"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('localhost', SERVER_PORT))
    sock.listen(10)

    while True:
        conn, addr = sock.accept()
        data = conn.recv(4096)
        if data:
            queue_.put(data)

def get_portnumber_of_maya():
    for portnumber in PORT_RANGE:
        if is_opened_port(portnumber):
            return portnumber
    # すべてのPort候補を探して、
    # 開いているPortが見つからなかったらエラーをだす。
    else:
        raise RuntimeError('No exists opened port of Maya.')

def run_server(command):
    maya_port = get_portnumber_of_maya()

    queue_ = Queue()
    queue_.put(command)

    # サーバーを立てて、送られてきた文字列をQueueに貯める
    thread = Thread(target=putter, args=(queue_,))
    thread.setDaemon(True)
    thread.start()

    # Queueにたまったコマンドを送信する
    thread = Thread(target=sender, args=(queue_, maya_port))
    thread.setDaemon(True)
    thread.start()

    while True:
        # queue_の中身がなくなるまで待機
        queue_.join()

        # 一定秒Queueに追加されないことを確認して処理を終了する
        for _ in range(WAIT_SECOND):
            time.sleep(1)
            # queueが空では無ければ待ち状態に戻す
            if not queue_.empty():
                continue
        break

def run_client(command):
    """クライアントモードで実行する。"""
    send_to_port(SERVER_PORT, command)

def doIt(command):
    if is_opened_port(SERVER_PORT) is False:
        run_server(command)
    else:
        run_client(command)

if __name__ == '__main__':
    if len(sys.argv) == 1:
        print('コマンド文字列を第一引数として渡してください。')
        ValueError('Pass the command string as the first argument.')

    command = sys.argv[1]

    doIt(command)

```

## 最後に

というわけでこんな感じでした。

せっかくなので基礎から調べなおそうと思い、rpycとか、
maya.app.general.commandportとかを眺めていたら、
記事を書く方針が全く定まらず、
結局フラフラとした記事になっていまいましたが、なにかの参考になれば幸いです。

(正直、全然満足できないので、後日リベンジしたいと思います。ご期待ください。)

## 躓いたことのQ&A形式で。

Q: cmds.commandPortで既にimportしたモジュールの関数を実行したけど、未定義エラーが帰ってきた！
A: cmds.commandPortで実行する処理は別の名前空間扱いらしく、実行する前にimportする必要がある。

Q: "from maya import cmds;cmds.polyCube()"を送って実行したら、socket.recvの戻り値が来ない  
A: 中身を見ていないので、細かいことはわからないが、1行目の結果だけ返しているっぽい？
　１行ずつ送信すると問題なく結果を返してくれる。

Q: 複数起動に対応しても、どのMayaに送るのか決める仕組みがなければ無意味じゃね？
A: 仰る通りでございます。
　　プロセス環境変数に優先するフラグを立てておいて、
　　Maya上で優先フラグを有効にするとポートを経由で他のMayaの優先フラグを殺させて、
　　Mayaのポートを探す段階で優先フラグを確認するようにすれば、どうにかなる。どうにかした。