名称はめだまライブラリなんですけどね
とりあえずまだまだ課題は残っているのですが(と、あらかじめ予防線をはりつつ(ぉぃ))、目玉ライブラリ(Medama) という名称で GitHub で ObservableSSH を公開しました。 目玉ライブラリという名称なので、さりげなく ObservableSSH 以外の機能も入ってます。とりあえず概要から導入まで含めてご紹介させていただければと思います。
概要
drieseng さんと olegkap さんのC#によるSSH実装であるSSH.NETと、neuecc さんの Unity用に実装されたReactive Extensions(GitHub)(アセットストア)を使用して、非同期なSSH通信をサポートします。単発コマンドだと割と簡単に使えますが、非同期なので tail -f /var/log/php.log 的なサーバ監視アプリとしても使えそうです。
具体的な動作としては、目的のサーバにSSHで接続したあと、標準出力を Observable
もすこし噛み砕いて説明すると、コマンド入力待ちの行は改行コードが無いため、Unity の標準出力イベントとして落ちてきません。そこで、EndOfStream イベントが来た時に、読み込みバッファの末尾が「]$ 」(角カッコ閉じ、ドル、半角空白)に一致すると、コマンド入力待ちとみなす。という風に使うわけです。具体的なコードについては後述します。
又、拡張しやすいFSM : 『Arbor 2』を使用して、ビジュアルシェルスクリプトもどきのクラスも作ってみました。こっちは1ノード1コマンドだと少し効率が悪いのでシェルスクリプト毎にノードを用意できるクラスが必要かなと。あとは、サーバ接続が切れた時の再接続・復帰ができたらいいですね。さらに、異常を検知したら処理を中断してアラートをあげてくれると最高かも。
その他の機能としては、サンプル表示用に uGUI を XML から自動生成する機能もあります。HTML 的に書けたらいいなーと思ってがんばって実装中です。 こちらは色々最適化や不要メソッドの削除は進行中なので、大幅に仕様が変わる可能性があります。
機能一覧
- XMLからuGUI自動生成(ヒエラルキにCanvasとEventSystemの自動生成もやります)
- スレッド型ObservableSSH
- マイクロコルーチン型ObservableSSH
- Arbor2によるObservableSSH制御
インストールの大まかな概要
- Unity5.6を使用します
- TortoiseGitを使用します
- VisualStudio2017を使用します
- 目玉ライブラリをGitHubからCloneします
- SSH.NETをGitHubからCloneしてVisualStudio2017でDLLをビルドしてUnityプロジェクトに突っ込みます
- UniRxをアセットストアからダウンロードしてインポートします
- Arbor2をアセットストアからダウンロードしてインポートします(オプション)
インストール
目玉ライブラリをGitHubからClone
ここでは E:\temp 以下に Clone するという前提で進めます。ファイルエクスプローラの何も無い空間を右クリックして GitClone をクリック
URL に https://github.com/medamap/Medama.git と入力し、OK をクリック
Clone が終わったら OK をクリック
Unity5 を起動して、Open をクリック
先ほど Clone したフォルダを選び、フォルダの選択をクリック
UniRxのインポート
UnityのWindowメニューをクリックし、Asset Storeをクリック
Asset Store が開くので、検索キーワード入力欄に UniRx を入力して Enter キーを押下する
検索結果のちょっと下の方に表示されているので、少し下にスクロールさせて UniRx を見つけたらクリック
UniRx をダウンロードしてインポートしましょう(この例はすでに何度も取得してるので最初からインポートと表示されています)
Importをクリック
Arbor2のインポート
Arbor2対応はこれから充実させていく予定なのですが、このアセットをインポートしない場合は目玉ライブラリの Assets\Plugins\Medama\Scripts\ArborScripts を削除する事でエラーが出なくなります。(アセットの存在を自動検知で条件コンパイルする機能があるといいなぁ)
Asset Store のホームボタンをクリック
検索キーワード入力欄に Arbor2 を入力して Enter キーを押下する
検索結果のちょっと下の方に表示されているので、少し下にスクロールさせて UniRx を見つけたらクリック
Arbor2 をダウンロードしてインポートしましょう(この例はすでに何度も取得してるので最初からインポートと表示されています)
Importをクリック
SSH.NETをGitHubからCloneしてDLLビルドしてインポート
再び E:\temp をファイルエクスプローラで開き、何も無いところを右クリックして Git Clone をクリック
URL に https://github.com/sshnet/SSH.NET.git を入力し、OK をクリック
Clone が完了したら SSH.NET\src に移動し、Renci.SshNet.VS2017.sln をダブルクリックして VisualStudio2017 を起動する
VisualStudio2017 で無事開けたら、ソリューションエクスプローラーの Renci.SshNet.NET35 を選択し、Release ビルドを選択する
ソリューションエクスプローラーの Renci.SshNet.NET35 を右クリックし、ビルドをクリック
Unity のプロジェクトビューの Plugins を右クリックし、Create → Folder をクリック
新規フォルダ名を Renci.SshNet にする
先ほど VisualStudio2017 でビルドした DLL が E:\temp\SSH.NET\src\Renci.SshNet.NET35\bin\Release にできているので、Renci.SshNet.dll を Unity のプロジェクトビュー Plugins/Renci.SshNet にドロップしてインポートする
Unity プロジェクトビューの Plugins/Renci.SshNet フォルダの左隣の黒三角をクリックしてツリーを展開し、Plugins/Renci.SshNet/Renci.SshNet を選択し、インスペクタービューの設定を図の通り(Any Platform をチェック状態、Exclude Platforms のチェックを全て外す)にして Apply をクリック
以上で、とりあえず使える状態にもっていけます。
サンプルシーン
あらかじめ、Assets\Plugins\Medama\Examples にいくつかサンプルシーンを入れてますので、適当に動かしてみてください。ちなみに、SSH 接続用のサーバは自前でご用意くださいませ。あ、秘密鍵ログインにまだ対応してなかった。とりあえず、ローカル環境のサーバ向けということで(ぉぃ)
使い方の例
uGUI自動生成
XMLからuGUIを自動生成します。自動生成されたuGUIは純粋なuGUIで、継承したカスタムとかじゃないのでここからさらに独自機能とかつけやすいかもしれません(かもしれません)
XMLの文字列をパースしてUIツリーを作成し、Canvas を自動作成(すでに存在してたらそれを採用)して UI をぶらさげます。 戻り値は Dictionary
using System.Linq; using UnityEngine; using UnityEngine.UI; using Medama.EUGML; public class eugml : MonoBehaviour { void Start () { // Create UI from XML. var dc = gameObject.MedamaUIParseXml(@"<?xml version='1.0'?> <uGUI xmlins='http://megamin.jp/ns/unity3d/ugui/eugml'> <!-- Window form --> <AddNode name='FormMain' sprite='resources://Medama/EUGML/UI001#Window001' layout='StretchStretch' top='8' bottom='8' left='8' right='8'> <!-- Title bar --> <AddNode name='TitleMain' sprite='resources://Medama/EUGML/UI001#Header001' layout='TopStretch' height='24' top='8' left='8' right='8' spritetype='Simple'> <AddNode name='Text' layout='StretchStretch'> <SetText textstring=' [__] TEST SSH LOGIN' color='white' alignment='MiddleLeft' /> </AddNode> </AddNode> <!-- Contents --> <AddNode name='GroupMain' sprite='resources://Medama/EUGML/UI001#Group001' top='40' bottom='8' left='8' right='8'> <!-- Input host address --> <AddNode name='TextLabelHost' layout='TopLeft' width='100' height='30' top='8' left='8'> <SetText textstring='HOST : ' alignment='MiddleLeft' /> </AddNode> <AddNode name='InputHost' sprite='resources://Medama/EUGML/UI001#Text001' layout='TopLeft' width='160' height='30' top='8' left='108'> <SetInputField /> </AddNode> <!-- Input user name --> <AddNode name='TextLabelUser' layout='TopLeft' width='100' height='30' top='48' left='8'> <SetText textstring='USER : ' alignment='MiddleLeft' /> </AddNode> <AddNode name='InputUser' sprite='resources://Medama/EUGML/UI001#Text001' layout='TopLeft' width='160' height='30' top='48' left='108'> <SetInputField /> </AddNode> <!-- Input password --> <AddNode name='TextLabelPassword' layout='TopLeft' width='100' height='30' top='88' left='8'> <SetText textstring='PASSWORD : ' alignment='MiddleLeft' /> </AddNode> <AddNode name='InputPassword' sprite='resources://Medama/EUGML/UI001#Text001' layout='TopLeft' width='160' height='30' top='88' left='108'> <SetInputField /> </AddNode> <!-- Submit button --> <AddNode name='ButtonLogin' sprite='resources://Medama/EUGML/UI001#Button003' layout='TopLeft' width='160' height='30' top='128' left='8'> <SetButton textButton='LOGIN' /> </AddNode> </AddNode> </AddNode> </uGUI>"); // Get UI components. // * Caution: Null not checking * var inputHost = dc .Where(gopair => gopair.Value.name == "InputHost") .First() .Value .GetComponent<InputField>(); var inputUser = dc .Where(gopair => gopair.Value.name == "InputUser") .First() .Value .GetComponent<InputField>(); var inputPassword = dc .Where(gopair => gopair.Value.name == "InputPassword") .First() .Value .GetComponent<InputField>(); var buttonLogin = dc .Where(gopair => gopair.Value.name == "ButtonLogin") .First() .Value .GetComponent<Button>(); } }
スレッド版ObservableSSH
次はObservableSSHの使い方です。サーバに接続するので、接続先情報を入力するための UI を表示させます。先ほどのサンプルは XML をプログラムの中に埋め込んでいて非常によろしくないので、 Resources に配置して XML を読み込む形式にします。 Assets\Plugins\Medama\Resources\Medama\EUGML\login.xml を用意します。
<?xml version='1.0'?> <uGUI xmlins='http://megamin.jp/ns/unity3d/ugui/eugml'> <!-- Window form --> <AddNode name='FormMain' sprite='resources://Medama/EUGML/UI001#Window001' layout='StretchStretch' top='8' bottom='8' left='8' right='8'> <!-- Title bar --> <AddNode name='TitleMain' sprite='resources://Medama/EUGML/UI001#Header001' layout='TopStretch' height='24' top='8' left='8' right='8' spritetype='Simple'> <AddNode name='Text' layout='StretchStretch'> <SetText textstring=' [__] TEST SSH LOGIN' color='white' alignment='MiddleLeft' /> </AddNode> </AddNode> <!-- Contents --> <AddNode name='GroupMain' sprite='resources://Medama/EUGML/UI001#Group001' top='40' bottom='8' left='8' right='8'> <!-- Input host address --> <AddNode name='TextLabelHost' layout='TopLeft' width='100' height='30' top='8' left='8'> <SetText textstring='HOST : ' alignment='MiddleLeft' /> </AddNode> <AddNode name='InputHost' sprite='resources://Medama/EUGML/UI001#Text001' layout='TopLeft' width='160' height='30' top='8' left='108'> <SetInputField /> </AddNode> <!-- Input user name --> <AddNode name='TextLabelUser' layout='TopLeft' width='100' height='30' top='48' left='8'> <SetText textstring='USER : ' alignment='MiddleLeft' /> </AddNode> <AddNode name='InputUser' sprite='resources://Medama/EUGML/UI001#Text001' layout='TopLeft' width='160' height='30' top='48' left='108'> <SetInputField /> </AddNode> <!-- Input password --> <AddNode name='TextLabelPassword' layout='TopLeft' width='100' height='30' top='88' left='8'> <SetText textstring='PASSWORD : ' alignment='MiddleLeft' /> </AddNode> <AddNode name='InputPassword' sprite='resources://Medama/EUGML/UI001#Text001' layout='TopLeft' width='160' height='30' top='88' left='108'> <SetInputField /> </AddNode> <!-- Submit button --> <AddNode name='ButtonLogin' sprite='resources://Medama/EUGML/UI001#Button003' layout='TopLeft' width='160' height='30' top='128' left='8'> <SetButton textButton='LOGIN' /> </AddNode> </AddNode> </AddNode> </uGUI>
上記 XML と、今までのインストール作業がうまくいっていれば、下記ソースそのままで動くはずです。(よくある、「ここは基本的な事なので省きます」とか「using は省略しますとか、『ムキー、ここが一番知りたいんじゃ!!』と度々経験するアレを華麗に回避」)
スレッド版は OnDestroy() の中で明示的に Dispose しないと Unity のプロセスが固まります。ここ改善されるまでは多分使い勝手はマイクロコルーチンの方が良いかも。
using UnityEngine; using UnityEngine.UI; using Medama.ObservableSsh; using Medama.EUGML; using System.Collections.Generic; using System.Linq; using UniRx; public class command_thread : MonoBehaviour { ObservableSSH ssh; public Queue<string> commands = new Queue<string>(); void Start() { // Create UI. var loginxml = Resources.Load<TextAsset>("Medama/EUGML/login"); var dc = gameObject.MedamaUIParseXml(loginxml.text); // Get UI components. // * Caution: Null not checking * var inputHost = dc .Where(gopair => gopair.Value.name == "InputHost") .First() .Value .GetComponent<InputField>(); var inputUser = dc .Where(gopair => gopair.Value.name == "InputUser") .First() .Value .GetComponent<InputField>(); var inputPassword = dc .Where(gopair => gopair.Value.name == "InputPassword") .First() .Value .GetComponent<InputField>(); var buttonLogin = dc .Where(gopair => gopair.Value.name == "ButtonLogin") .First() .Value .GetComponent<Button>(); // Regist command queue. commands.Enqueue("ps"); commands.Enqueue("rpm -qa"); commands.Enqueue("df -h"); // Button click event. buttonLogin .OnClickAsObservable() .Subscribe(_ => { // Initialize SSH. ssh = new ObservableSSH( host: inputHost.text, user: inputUser.text, password: inputPassword.text); if (ssh.con.IsAuthenticated) { // Monitor stdout. ssh .stdOutSubject .Subscribe(line => Debug.Log(line)) .AddTo(ssh.compositeDisposable); // Check stream status and send shell command. ssh .statusSubject .Where(status => status == ObservableSshStatus.EndOfStream && ssh.CheckBuffer("]$ ") && commands.Count > 0) .Subscribe(status => ssh.writeSshSubject.OnNext(commands.Dequeue())); } // Double login prevention. buttonLogin.enabled = false; }) .AddTo(this); } // The thread version require dispose SSH on destroy. private void OnDestroy() { if (ssh != null) { ssh.Dispose(); } } }
マイクロコルーチン版ObservableSSH
内容スレッド版とほとんど同じですが、初期化の部分と OnDestroy() の部分が少し違います。マイクロコルーチン版は MonoBehabiour のクラスの中に ObservableSSH のプロパティを持っており、中身はイベントの処理方法がちょっと違うだけ程度です。
using UnityEngine; using UnityEngine.UI; using Medama.ObservableSsh; using Medama.EUGML; using System.Collections.Generic; using System.Linq; using UniRx; public class command_microcoroutine : MonoBehaviour { ObservableSSHMonoBehaviour ssh; public Queue<string> commands = new Queue<string>(); void Start() { // Create UI. var loginxml = Resources.Load<TextAsset>("Medama/EUGML/login"); var dc = gameObject.MedamaUIParseXml(loginxml.text); // Get UI components. // * Caution: Null not checking * var inputHost = dc .Where(gopair => gopair.Value.name == "InputHost") .First() .Value .GetComponent<InputField>(); var inputUser = dc .Where(gopair => gopair.Value.name == "InputUser") .First() .Value .GetComponent<InputField>(); var inputPassword = dc .Where(gopair => gopair.Value.name == "InputPassword") .First() .Value .GetComponent<InputField>(); var buttonLogin = dc .Where(gopair => gopair.Value.name == "ButtonLogin") .First() .Value .GetComponent<Button>(); // Regist command queue. commands.Enqueue("ps"); commands.Enqueue("rpm -qa"); commands.Enqueue("df -h"); // Button click event. buttonLogin .OnClickAsObservable() .Subscribe(_ => { // Initialize SSH. ssh = gameObject.AddComponent<ObservableSSHMonoBehaviour>(); ssh.InitializeAndStart( host: inputHost.text, user: inputUser.text, password: inputPassword.text); if (ssh.con.IsAuthenticated) { // Monitor stdout. ssh .stdOutSubject .Subscribe(line => Debug.Log(line)) .AddTo(this); // Check stream status and send shell command. ssh .statusSubject .Where(status => status == ObservableSshStatus.EndOfStream && ssh.CheckBuffer("]$ ") && commands.Count > 0) .Subscribe(status => ssh.writeSshSubject.OnNext(commands.Dequeue())); } // Double login prevention. buttonLogin.enabled = false; }) .AddTo(this); } // The micro coroutine version automatically dispose SSH. //private void OnDestroy() { // if (ssh != null) { // ssh.Dispose(); // } //} }
ObservableSSHのフィールド
型 | フィールド名 | 概要 |
ConnectionInfo | con | 接続オブジェクト |
SshClient | sshClient | SSHクライアントオブジェクト |
bool | loop | SSH読み込みストリームを強制終了させたい時はこれを false にする |
ShellStream | shellStream | シェルストリーム。これから read や write のストリームを生成する。また、読み込みが落ち着いたかどうかもこのオブジェクトで判断できる |
Subject<string> | writeSshSubject | クライアント側から SSH コマンドを送信したい場合は、writeSshSubject.OnNext(“ls -la”); 的に使う |
Subject<string> | stdOutSubject | サーバ側から改行がくる度に1行分の標準出力がイベントとしてやってくる。(これのせいで、1行がバッファを越える分だけ受信するとそこで処理が止まるという既知の問題アリ、さて、どうしたものか) |
Subject<ObservableSshStatus> | statusSubject | SSH 接続状態のステータスを受け取るためのストリーム。今のところ、読み込みが落ち着いた程度しかないので、切断されたとか、再接続制御とかにも使えるようにしたい |
string | commandPrefix | SSH コマンドの頭に強制的につける文字列。例えば、「$ env LANG=en_US.UTF-8 ls -la」みたいに英語設定で結果を出したい時とかに使用する |
CompositeDisposable | compositeDisposable | Subscribe したのを Dispose する用 |
StreamWriter | stdInStreamWriter | SSH 書き込みストリーム |
ReactiveProperty<Buffer> | observableSSHBuffer | SSH読み込みストリームから読み込む度にこいつに通知され、こいつの中で改行で終わる1行分の文字列を取得できたらstdOutSubject に OnNext してくれる |
string | waitstring | 未使用。多分消すかも |
サーバの自動設定とかこいつで色々できると便利と思って色々実験中
Unity で SSH 通信ができるということは、フロントを擬人化した Live2D キャラクターにしておいて、攻撃されてる時は具合悪そうにしたり、ストレージがそろそろヤバい時教えてくれるそういうアプリもありかもしんないですね。
