スマホでデバグ用Httpサーバ(2) ファイル階層表示とその場編集

f:id:hirasho0:20190614180745p:plainf:id:hirasho0:20190614180747p:plain

こんにちは。技術部平山です。

今回はhttpサーバをスマホで動かす記事の続編です。 テキストファイルをブラウザ上で編集できるようにし、 また、StreamingAssets以下のファイル階層を見られるようにしました。 テキストならその場で編集できますし、それ以外でも、 アップロードによって置換することができます。

まだまだ未完成で実戦投入もしていないのですが、 誰かに反応をもらってそれを開発に活かしたいということで、 とりあえず記事にしてみることにしました。

コードはgithubにサンプルプロジェクトの形で置いてあります。 中途半端な状態でブログを書いた際に、それに対応したコードを どう残しておくか、あるいは改訂するのか、 といったことはまだ決め切れておりませんが、 基本的には古いコードはブランチで残し、上書きしてしまう方向で行く予定です。

今回の改良

前の記事では、ファイルのアップロードと、その削除だけを実装しました。 「3面のステージデータを修正して実行したい」と思えば、 PCでそのファイルを編集し、ブラウザのページで行き先のパスを指定し、 ファイル選択ダイアログでファイルを選んでから、送信ボタンを押す、 という手間が必要でした。

f:id:hirasho0:20190614180750p:plain

しかし、 「1クリックでも減らせ」「間違えることができる工程を減らせ」 というのがツール作りの基本かと思いますので、こんな状態では全然ダメです。

  • 行き先を指定するのが面倒くさいし間違う。
    • 現状間違えてもアップロードできてしまう
    • 行き先のパスなんてたぶん覚えてない
  • ちょっとした変更なら別途エディタで編集して保存するのは面倒くさい
    • 多くの変更は数字を変えてみることのはずで、数文字の変更だったりする。

というわけで、今回はそこの改善を試みたわけです。結論から言えば、

  • macのfinder、windowsのexplorerみたいなものの簡易版をブラウザ上に用意する。
    • 文字列を入れずにファイルを選択できる。
  • テキストファイルはその場で編集できるようにする。

という2点の機能追加を行いました。ファイル選択は上の画面写真の 「ファイル選択」というリンクから行えます。

実装

前回作ったDebugServerは、サーバ上のパスとコールバックをセットにした 辞書を持ち、パスに対応したコールバックを呼ぶだけの代物でした。 機能の実装はユーザに丸投げ状態で、アップロードも削除も ユーザ側にありました。 サンプルだとライブラリとユーザコードの境目がよくわからないかもしれませんが、 Kayacディレクトリ以下 はライブラリとしてアプリではいじらないことを想定しています。 package化して配るイメージです。

今回は、このライブラリ内の機能を大きくし、 「特定フォルダ以下はファイルアクセスとして専用のクラスが担当する」 作りにしました。

DebugFileService というクラスで、例えば/assets/以下のパスが来たら、 それをStreamingAssets内のファイルパスであると解釈して動作します。 例えば、http://192.168.1.5/assets/StageData/stage1.json は、StreamingAssets/StageData/stage1.jsonと解釈し、 jsonなので編集フォームを出して、ファイルの内容をフォームに入れておきます。

HTML生成

今回はファイルのリスト生成等でどうしても動的にHTMLを吐く必要があり、 どうやってやるかが問題になりました。

このサンプルではとりあえずSystem.Xml.XmlWriter を使っています。誰かがもっと用途に合ったものを配っているに違いないとは思いつつ、 ライセンスを調べるのも面倒ですし、「標準である」ということは重大です。 所詮デバグ用だし、まあこれでいいかなと思います。

さて、XmlWriterには結構機能があるのですが、 今回くらいの用途だと使う関数/機能は以下のものくらいです。

例えば、こんな感じのコードを書くと、

var sb = new StringBuilder();
var settings = new XmlWriterSettings();
settings.Indent = true;
settings.IndentChars = "\t";
settings.NewLineChars = "\n";
settings.OmitXmlDeclaration = true; //<?xml ... を吐かない
var writer = XmlWriter.Create(sb, settings);
writer.WriteDocType("html", null, null, null);
writer.WriteStartElement("html");
writer.WriteStartElement("head");
writer.WriteStartElement("meta");
writer.WriteAttributeString("charset", "UTF-8");
writer.WriteEndElement(); // metaを閉じる
writer.WriteElementString("title", "DebugService");
writer.WriteEndElement(); // headを閉じる
writer.Close();

以下のようなhtmlの断片ができます。

<!doctype html>
<html>
<head>
   <meta charset="UTF-8">
   <title>DebugService</title>
</head>

よく使うパターンに合わせて、XmlWriterの関数呼び出しをいくつかまとめた 便利関数を用意しておけば(HtmlUtil) 、まあ許容できる手間でhtmlを生成できます。

例えば、A要素であれば、

public static void WriteA(XmlWriter writer, string hrefAttribute, string innerText)
{
    writer.WriteStartElement("a");
    writer.WriteAttributeString("href", hrefAttribute);
    writer.WriteString(innerText);
    writer.WriteEndElement();
}

こんな関数があれば、それほど苦痛でなくなるでしょう。

なお、DOM形式(XmlDocument を使うやり方)も試しましたが、結局XmlWriterが必要な上にコードが長くなるので、 あんまりうれしくありません。

StringBuilderに直接htmlを書きこむ手もありまして、 コードの長さ自体はその方が短いのですが、 ダブルクォーテーションやタブが混ざったかなり辛いコードになります。

javascript書き出し

javascriptの書き出しはなかなか厄介です。 しかし、これに関しては前もって用意した文字列をそのまま埋め込むことにしました。 今のところ定型のコードしかないからです。 文字列リテラルに数行以上あるコードを入れるのはなかなか邪魔くさいのですが、 @付きのリテラル(verbatim string literals) のおかげで多少は楽ができます。

const string script = @"
var log = document.getElementById('log');
var onUpdate = function () {
  var request = new XMLHttpRequest();
  request.onload = function () {
      log.value = '編集受理\n';
  };
  request.onerror = function () {
      log.value = '編集失敗\n';
  };
  request.open('PUT', document.location.href, true);

  var textArea = document.getElementById('text');
  request.send(textArea.value);
};";

中に\があってもエスケープと解釈されないので、 コードを書きやすいのです。

とはいえ、C#のコード中に唐突にこんなものが出てくるのは嫌ですね。 何か素敵な方法で解決したいものですが、 いちいち別ファイルにしてTextAssetの参照を介してデータをもらう、 なんてことをするのはさすがに大袈裟すぎます。 とりあえずは仕方ないかな、という感じです。

url最後のスラッシュ問題

web関連技術者の方々にとっては常識なのでしょうが、 urlの最後にスラッシュがあるかないかで結構な違いがあります。

http://hoge/StageData/にアクセスした時、 カレントディレクトリは/StageDataです。 なので、html中に<a href="fuga.png">とあれば、 それはhttp://hoge/StageData/fuga.pngを指します。

一方、スラッシュがないhttp://hoge/StageDataの場合は、 カレントディレクトリは/になります。 なので、そこで<a href="fuga.png">とあれば、 それはhttp://hoge/fuga.pngを指します。

スラッシュを書き忘れてもStageDataの中身を表示してやろう、 という親切は良いと思うのですが、吐くhtmlに書いたファイルリストの リンクを/付きの時と同じにすると、ファイルが見つからずにエラーになります。 この挙動を理解するのに1時間ほどかかりました。 最後に/があるかどうかでa要素のhref属性の値を変えないといけないわけです。

とはいえ、今回はそんな面倒なことはしたくなかったので、 HttpListenerResponse.Redirect() を使うことにしました。 /がない時には、/がついたurlにRedirectします。

url += "/";
response.Redirect(url);

こうすると、ブラウザのアドレスバーに/なしのurlを書いても、 /付きのurlに飛ばされたかのように動作します。 この関数の中でStatusCodeがRedirect(302)にされるので、 その後でHttpListenerResponse.StatusCode をいじらないように注意が必要です。

AndroidはStreamingAssets内を検索できない問題

今回一番面倒だったのは、Android対応です。 AndroidではStreamingAssets以下がアプリの巨大なファイルの中に埋め込まれていて、 普通のSystem.IOの機能では読めません。 System.IO.Directory が持つ Exists()GetDirectories()GetFiles() が使えません。

つまり、あるディレクトリを指定された時に、 それが存在しているかもわからず、その中に何があるかもわからないのです。 これではファイルやディレクトリのリストを返せません。

実装

結局、System.IOを使わずにどうにかすることにしました。

  • エディタ上でStreamingAssets以下に何があるかをjsonに書いておく
  • jsonはResourcesの下に入れておく
  • DebugFileService起動時にjsonを読んでテーブルを作っておく
  • ディレクトリの中を見る要求が来たら、そのテーブルを見てディレクトリの中身リストを作る

という感じです。

jsonを書くエディタ拡張はStreamingAssetsMap.csに用意しました。

まずstaticな関数GenerateMap() を用意し、これにMenuItem属性をつけて、メニューから実行できるようにします。

次に、自動で呼ばれるように、 IPreprocessBuildWithReport を実装して、ビルド前に自動で呼ばれるようにします。 IOrderedCallback.callbackOrder の実装も必要です。今回は0を返しておきました。

さらに、エディタで実行開始する時にも自動で呼ばれるように、 EditorApplication.play/ModeStateChanged にコールバックを登録します。

static StreamingAssetsMap()
{
    EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}

static void OnPlayModeStateChanged(PlayModeStateChange state)
{
    if (state == PlayModeStateChange.EnteredPlayMode)
    {
        GenerateMap();
    }
}

staticコンストラクタでイベントに関数を足し、実行開始時にjsonを生成するようにしました。

json生成及び、jsonからのデシリアライズ、実行時の検索、 については説明すると長くなるし原始的なコードなので、 興味がある方はコードをご覧ください。

補足: JsonUtilityの制約と木の表現

ここで、一つだけコードを読むだけではわかりにくい点があるので補足します。 json化とjsonからのクラスインスタンス生成に、 JsonUtility を使っている関係上、少々工夫が必要だということです。

JsonUtilityは、ある型に、その型自身の参照が入っている場合を扱えません。

[System.Serializable]
class Hoge{
    public Hoge next;
}

こういうのはダメです。HogeにHogeが入っています。

しかし、今回やりたいのはファイルシステム情報の格納です。 ファイルシステムという奴は、 ディレクトリの中にディレクトリが入っているわけで、 自然に書けば上のような形になってしまいます。

他のJsonライブラリを使えばいいのですが、JsonUtilityの速度は魅力的ですし、 何よりも他のものを持ってきて入れるのは面倒です。 そこで、古臭い手法を引っぱり出して、 無理矢理JsonUtilityを使うことにしました。

[System.Serializable]
class Directory
{
    public string name;
    public int firstChild;
    public int nextBrother;
}

[System.Serializable]
class Map
{
    public List<Directory> directories;
}

firstChildはそのディレクトリが含むサブディレクトリの最初のもの、 nextBrotherは、自分と同じディレクトリにある、次のディレクトリです。 なぜintかと言えば、Directoryへの参照を直接持たず、 Mapクラスが含むListのインデクスを代わりに持つためです。

  • A
    • B
    • C

というディレクトリ構成は、

{
    "directories":[
        {
            "name":"A",
            "firstChild":1,
            "nextBrother":-2147483648
        },
        {
            "name":"B",
            "firstChild":-2147483648,
            "nextBrother":2
        },
        {
            "name":"C",
            "firstChild":-2147483648,
            "nextBrother":-2147483648
        },
 ]
}

とシリアライズされます。Aの最初の子はBで、これは1番です。 CもAの子で、これはBのnextBrotherであり、2番となります。 nullが使えないので、代わりにint.MinValueすなわち-2147483648 を入れてあります。デシリアライズ時には「マイナスだったらnull」 とすればいいわけです。 このようにして、「最初の子と兄弟」形式のデータを配列にすることで、 木をシリアライズできます。 デシリアライズはこれを逆に辿るだけです。

なお、木の表現としては、各ノードに子の配列を持つ形式もあります。 直感的ではあるのですが、各ノードの中に配列がさらに入ってjsonが複雑化し、 メモリ効率も悪くなります。私は「最初の子と兄弟」形式が好きです。

JsonUtilityの制約について公式情報はどこ?

ところで、JsonUtilityに関する制約の公式情報はどこにあるのでしょう? マニュアル には「同じ型を入れてはダメ」なんて書かれていないのです。 でもやってみたら出ませんでした。

[System.Serializable]
class Hoge
{
    public List<Hoge> hoges;
}

のようにListを間に挟んだりすると出せるようになるのですが、 その場合は「7階層が限界です」という警告が出ます。

f:id:hirasho0:20190614180753p:plain

そんなわけで、実装は面倒くさいが間違いなく大丈夫な今回の やり方を選んでみた次第です。

今後

今回の改造で多少使い勝手がマシにはなりましたし、 HTMLを吐く関連のコードを整備したことで、 今後の作業がやりやすくもなりました。

さて、次はどうしましょうか。

hierarchyウィンドウとinspectorウィンドウをブラウザに出す

動的にHTMLを作るのがそれほど苦痛でない、となれば、 hierarchyウィンドウみたいなものをブラウザに出して、 シーンやgameObjectの一覧を出したくなってきます。 木構造はulとli要素で表現できますから、 面倒くさいだけで難しいことはないでしょう。

inspectorに関しては、選択中のgameObjectに対して GetComponents() して、それに対してリフレクションでフィールドを列挙して、 出せる変数を片っ端から表示すれば良いのでしょう。 privateフィールドであってもリフレクションを使えば無理矢理変更できるらしいので、 やればできそうな気配です。

JSONをテキスト直でない形で編集したい

世の中にはJsonのエディタが存在しており、 例えばJSON Editor Online のようなものがあります。

素でテキストを編集するのは非プログラマにとっては苦痛ですし、 間違ってカンマを消したり、falseをfolseとスペルミスしたり、 ダブルクォテーション(")をつけ忘れたりと、まあ悲惨な未来が見えます。 そこで、間違いようがなく、使いやすいUIを提供できると良いと思うわけです。

さらに、型定義や、値の範囲制限などもあると最高ですね。 「enemyCountは0以上100以下」みたいなことをルールとして与えておき、 そこから外れた値はそもそも入力できなくするのです。

しかし、さて実際どうやって実装するか?と考えると結構気が重いものがあります。 型定義や範囲チェックをまずは考えないとしても、 要素の追加が必要な段階で、静的なHTMLを吐いて終わりにはできないでしょう。 ユーザの入力に応じてjavascriptでhtmlの構造をいじる必要があり、 かなり大きなjavascriptを吐かないといけない気がします。 気が重いですね。

もしやるとすれば、jsonでなくyamlにする、というのはどうでしょう。 コメントも書けますし、jsonよりは手書きでミスをしにくい作りです。 YamlDotNet というライブラリも存在しています。

json代わりに使うだけならyamlの全機能は不要なので、 もっと高速な独自実装を用意したい気持ちが湧いてきますが、 やめた方がいいですね。すぐ自作したくなるのは私の悪いクセです。

終わりに

web素人なクライアント屋が片手間でできる範囲でやっており、 それでも開発の効率化にある程度は寄与できると思うのですが、 じわじわと「片手間」の限界が見えてきた感じがあります。

非プログラマが使えるツールを作るには、 UIの見栄えまで含めて、かなりの磨き込みが必要です。 でないと、使い方を教える/覚えるコスト、トラブル対応コスト、 ミスった時の損失、等々のために役立たずになってしまいます。 まあ、雑なツールでも使える範囲というのはあるので、 「磨き上げないと全く使えない」ということはないのですが、 もうちょっとどうにかしたいですね。JSON素のまま、というあたりは特に。

そろそろweb系の仕事をしている技術者に協力を仰いでみても 良いのかもしれません。弊社はそういう人材が豊富ですし。