続々々々々続GShell — 動的リンク

開発:さて今日は、GShellの動的リンク機能にチャレンジしたいと思います。

社長:タイムスリッパでやってたのがちょうど一ヶ月前ですね。

開発:それがもう、恐ろしいくらいに何も覚えてないのですが、ブログと、残されたコードを頼りにして行きたいと思います。

基盤:一ヶ月前というとあおの血染めの帰宅の頃ですが、あの時脳震盪で記憶が飛んだとか?

開発:それでまず、Goプログラム自体を動的リンクモードで生成するという話が気になっていたのでこれを…

開発:そうですか。ではLinuxで。

社長:尋常では無い遅さですね。

開発:gshell自体に動的リンクしようという話と方向違いですからね。まあでも、macOSだと3ミリ秒のところが、Linuxなら1ミリ秒で起動するようです。

開発:されそれで、発掘しましたタイムスリッパ。7月13日05:50のタイムスタンプのあるlibgasket.so.1 です。これをかませてやると1970年にタイムスリップするはず…

開発:あれ?

基盤:しないですね。

開発:そういえばmacOSの場合には、ビルド時になにかやる必要があったような…

社長:いや、Jan 1 09:10:44 (1970) にスリップしてるんでは。

開発:あそうか… /bin/dateは動的ライブラリでスリップしてくれないという話でしたね。というか、さっきから DYLD_ 設定しなくても勝手にスリップするんですが… かと思うと他の端末ではそういう事はない…

社長:動的ライブラリのキャッシュっぽいですね。

開発:確かに。タイムスリッパ版 gettimeofday がキャッシュされていると思われるような症状です… そういえば DYLD_ほにゃららにキャッシュを無効にするというのがあったような… man dyld。まずはトレースですね。DYLD_PRINT_LIBRARIES= DYLD_PRINT_BINDINGS= gsh ・・・

基盤:うわわわ…

開発:発見しました。

社長:なるほど。macOSでは動的ライブラリが細分されているんですごい数をロードする、それで起動に3ミリ秒もかかるということですかね。

開発:ちょっと邪魔なのでdyldの出力は別ファイルに出します。 DYLD_PRINT_TO_FILE=dylog。うーむ、起動するまでに120ファイルの .dyld をロードしてます。1ファイル25マイクロ秒でロードできちゃってると… まあ、オンメモリだからなんでしょうけど。

基盤:macos dyld cache clear で検索。どうも実体は /private/var/db/dyld の下にありますね。1.4GBもあるんだがどうしたらいいんだみたいな質問が。うちのMacMiniはどうかと言うと…

一同:(苦笑)

基盤:でもこの中にgasketは無いんですよね。これはシステムのdylib専用のキャッシュなんですかね?こっちは3ヶ月使ってきたMacMiniですが、まだあんまり活動してない iMacはというと。

開発:タイムスタンプも大きさもほぼ同じですね。要するに Catalina 10.15.6 に共通ということですかね。何が違うんでしょう?

基盤:.map のdiffを取ると、まあアドレスは連鎖的に変わるでしょうけど、大きな違いは2つdylibが加わったということのようです。

社長:で、プライベートなキャッシュはどこにあるんですかね。端末というか、セッションごとにあるように見えますが。

基盤:ところでこのdyldって、なんて読むんですかね。ディル… って読むとヤバいと思うんですがw

開発:単にダイエルディーって読んでましたが。

開発:ああそれで、dyldinfo -bind gsh で、動的にリンクされているシンボルを眺めたのですが、やはり差し替えたら面白いと思うのは、ファイル名を解釈する部分、つまり openとexecveかなと思います。ホスト名についてはgetaddrinfoとgetpeername。これで名前空間をいじって簡易マウントする。それから、出力をteeしちゃうという意味ではwriteとsend。入力をteeするという意味では read と recvfrom。

社長:端末入出力の記録がわかりやすいので、read / write から行きましょう。

開発:了解。ではまず、gettimeofdayのラッピングは外して…

基盤:今日に戻りました(^-^)

開発:ああ、どうもGoで区切り記号を省略する癖がついてCでもやっちゃいますねw。むむ、read をラップして __read を呼ぶという戦法は Macのリンカーには通じないのかな… とりあえず read としてエラーを返すことにします。こんな感じ。

開発:で、これを動的ライブラリにしてgshにリンクさせると。

基盤:入れ替わってますね。

開発:さてでは、どうやって本物に中継するかですが。システムコールなら syscall でイケばy位とは思いますが。

基盤:7/12のタイムスリッパでも、Linux は func を入れ替えて本物は __funcを呼ぶでイケる、macOSは不明なのでペンディングということになってますね。

開発:うーん、dyldinfo を見ても、__ を前置するという流儀のように見えるんですが…

基盤:なんとなく、macOS固有の拡張シンボルにつけてるようにも見えますが。

社長:なんにしても、動的ライブラリがシンボルとしてエクスポートしてないとどうにもならないですね。

続々々々々GShell

開発:さて、今日はUnix的なるものの核心の一つ、pipe に取り組もうと思います。

社長:それは標準入出力と切っても来れない関係にありますね。

開発:pipeにはファイル名がありませんからね。予めオープンされたディスクリプタとしてしか獲得できない。それは普通、shellから標準入出力として、fd0 とかfd1としてプログラムに渡されるわけです。

社長:ディスク上に記録されないので無限長のデータ処理も可能。今はマルチコアの時代なので、pipeをはさんでまさにパイプライン並列な高速処理ができるようになりました。

開発:shell のパイプ記号 | のわかりやすさもあって 、出力と入力のプログラムを並行して起動してパイプでつなげるという方式は大成功を納めたと思います。

社長:半世紀くらい前に読んだ「Unixの四半世紀」という本でも、ベル研の人たちがこれを思いついた時に大喜びしてたシーンが書かれてた記憶があります。

開発:これがベースになって、いわゆるフィルターコマンドという形で各種のコマンド部品が蓄積されて行きましたね。各プロセスは単に入力を加工して出力に出すだけなので、書きやすいし、それを組み合わせるのは外で、通常はshellがやってくれて、いろんな組み合わせができる。

基盤:まあ、cat | sort | uniq -c | sort なんてのは今でもよく使います。grep も -e とか付けるのが面倒なので、grep | grep | grep … なんて継ぎ足していくことが多いですね。性能的にも問題無いし。

開発:ただこの、いろんな処理が一つにまとまっているのが簡便な一方、ちょっとフィルター的に収まらないプログラムを作る時に残念なことになるわけです。

社長:pipe記法の制約の一つは、a | b という記述で、a と b が同時に起動されないければいけないということだと思います。まあ、データを一気に上から下まで流すという使い方を想定すればそれで良いのですが、そうでないこともある。

開発:なので、このパイプ記法がワンセットとして実現している機能をプリミティブにばらして、ユーザがそれを組み合わせられるようにできると面白いだろうなと思うわけです。

* * *

基盤:ここで一つ、昨日入荷した桃ちゃんを頂きたいと思います。うーん、なんか香りが弱いですね。

社長:なんにしろ桃は皮ごと食べるのが常道です。ではひとくち。がりっ

基盤:あれ?めっちゃ固いですね。がりがり

開発:桃とは思えないこの食感。普通なら困ってしまうほどのジューシーさがまるで無い。ごりっ

社長:まあ、果物には当たり外れがありますけどね。しかしこれはひどい…

開発:あのスーパーは昔はこんなものを売るような店じゃなかったですが…

* * *

開発:さて、今日はちょっと地道な機能を追加していこうと思います。まず現状のまとめ。

開発:まず、which コマンドを拡充しまして、ワイルドカードが使えるようにしました

基盤:うーん、これは普通に便利。インストールされてるパッケージとかライブラリも同じようにできると良いですね。

開発:ヒットしたファイルの属性表示には、file コマンドに食わせるとかも良いかもですね。cksum とか wc とかいろいろ有り得る。まあ、xargsでやれるかも知れませんが、shellのビルトイン記法で。

社長:ワイルドカード検索した結果をフルパスでなくてコマンド名だけで出すモードも欲しいですね。その結果をまた、別のコマンドに食わせる。

開発:which は要するにfind の特殊形だと思うんです。逆に、which を表現できるようなビルトインのfindを作ると良いかなと思います。

開発:あとは、リソース使用量の表示について、ビルトインコマンドでも表示できるようにしました。

基盤:内部コマンドレベルのオーバヘッドは30マイクロ秒くらいですね。

社長:せっかくnopというコマンドを作るなら、アセンブラが使えると良いかもですね…

開発:ということで、とりあえず現状をアーカイブします。現在1175行。

開発:あともう一点。Goのビルドしたネイティブなバイナリの性能が気になっていたのですが、まあ立ち上がりについてはCからのネイティブと変わらないようです。

社長:CPUを使ってる時間はほぼ同等ですから、あとはバイナリがキャッシュにあるかどうかの違いが支配的ですね。

開発:CPUを一ミリ秒も食ってないのに起動・終了に3ミリ秒かかってしまっているのは、GShellの ForkExecとWaitのせいかも知れません。

雷:ピカドカドッカーングシャバリーン!!!

全員:びっくりしたー!

開発:確認のために、何もしない main()だけのプログラムでもはかってみましたが、同じようなものですね。

開発:確認のためにzshにて…

社長:3ミリ秒がこのMacMiniでのプロセスのfork+execの限界って感じですね。

基盤:この CPU %って、単にCPU時間を経過時間で割ったものみたいですが、便利だと思います。

開発:そうですね。というか、TIMEFMTは他のCPUと互換にします。

* * *

社長:さてみなさん、今日の昼食は昨日買ってきたとうもろこしであります。じゃーん。

開発:なんかイマイチ痩せたかんじですね。

基盤:では、レンジで。80度で5分くらいで良いですかね?

社長:もうちょっとかな?

基盤:ともかくレンジでGo!

レンジ:ゴー・・・

基盤:な、なんかめっちゃいい匂いがして来ました。

開発:ふくらむ期待・・・

レンジ:チーン

基盤:ぱかっと。あちっ、ムキムキ。あちちち。ちー。剥けました。

基盤:どれ一口。むしゃ。… ややナマっぽい。けど、うまーい!

開発:これはいけますねー。むしゃむしゃ。

社長:すばらしい。コンビニで売ってる茹でて長期保存のとは世界が違う。本物の世界ですね。むしゃむしゃ。

基盤:これは醤油をたらして焼きを加えたら最高でしょうね。

開発:ベランダに七輪を置きましょうかね。

社長:「写真の前に挿した桜の花かげに すずしく光るレモンを今日も置かう」智恵子抄はこの詩で終わってほしかった。

* * *

開発:ふぁ。あ。あー。いつの間にか、よく寝てしまいました。何かやろうと思ってた事を忘れてしまい…

基盤:スイカうまいですね。シャグ。

社長:今度は丸ごとに挑戦しますかね。

開発:外れたら悲しいですが、そこがワクワクもしますよね。

* * *

開発:それで、今朝方やってたパイプ関係の話ですが。プリミティブ的には、pipeを作る。pipeを入力とするプロセスを生成する。pipeを出力とするプロセスを生成する。これだけだと思います。

開発:たとえばこんな風になります。

社長:まあ、送る側も受け取り側も、途中で別人に交代しても悪くないのは確かですが。

開発:これで、cat a b c は必要なくなって、cat a ; cat b ; cat c でも良くなります。

基盤:猫いらず。

開発:それ次に、tee を考えます。

社長:cat は fan-in ですが tee は fan-out ですね。

開発:で、こういう風にプリミティブに分解してやると、接続関係は自習になります。メッシュ状にもループ状にも。

社長:私はそれで昔、π言語を使って接続を書きました。

基盤:端末から入力したい気はあまり起こらないですね。

開発:なにか新しい使い方が思いつくかもですよ。たとえば入力した数値を一つカウントダウンして出力するというプログラム。decとしましょう。

社長:なかなか本格的なプログラムですねw

開発:で、このプログラムの出力を自分の入力につなげてやると。

基盤:-i と -o じゃなくて < と > になったんですね。

開発:まあ、どっちでもいいんですが。脳内に刻まれてしまった特殊記号には勝てないですね。一文字だし。

社長:なんにしても、普通の shell では書けないことのようには思われます。

開発:namedpipe で出来るとは思いますけどね。

社長:socketpair なら双方向に通信できるから、何か面白いことができるかもですね。

開発:まあいずれにしても、標準入力を処理して標準出力に出すというある意味無敵な汎用モデルには勝てないですけどね。ただ、フィードバックループを持つプログラムは普通にありますから、それをshellでサポートしてコマンドレベルで部品にできれば意味があるようにも思います。

社長:標準制御入力とか標準ログ出力とかもあってよいですよね。要は、入力をselectしてどこからデータが来たらどうする、というのがプログラムの標準モデルとして提供されていないのが問題な気がします。

開発:そのへんは当然シミュレーション用の言語とかではあるわけですから、それをそのままshellに持ち込むでも良いかもですね。

社長:私が学生の時の研究室が貧しくて、シミュレーション用の言語の処理系が買えなかったんです。だもんでSimulaのサブセットを自分で付くて使ったりしてました。

基盤:コロナでお亡くなりになってしまった方ですね。

* * *

社長:ところで、どうも気に入った壁掛け時計が無いんで、自作したいなと思うんですが。

基盤:気温とか、電力消費とかも表示すると良いですね。

社長:いつものデスクトップに表示するのは、ちょっと違うかなと思うわけです。

開発:なんてったって憧れるのは電光掲示板ですよね。やはりLEDの発光には魅入られます。小さいのなら 3000円位で手に入るみたいですが。こういうの。

開発:ラズパイで制御したいと思います。

社長:64 x 32 ってことは、ちっちゃいアイコンの情報量ですね。

基盤:ちっちゃいLCDモニタなら5000円くらいでありますよね。ああ、HDMIで7インチ3000円近辺がふつうみたいですが。

社長:そういうの、前の職場で使ってましたが、普通に良かったです。LEDにしましょう。

基盤:タッチパネル機能付きで7000円のもありますね。ラズパイ愛好者にオススメ。

開発:その写真の手の人はコビトさんですかね?

社長:これにしましょう。

経理:溜まってるアマゾンポイント全投入します。ぽちっ。金曜日にお届けとのこと。

基盤:楽しみ!

社長:今気づいたんですが、今日は水曜日なんですね。昨日は月曜だと思っってボウリングの試合に出かけたのですが…

開発:どこかで一日タイムスリップしたようです。

基盤:ブログに空白日は無いようですが…

社長:試合を休んでチームに迷惑をかけてしまいました。今たぶん、2位につけているはずなのですが…

* * *

社長:ところで、gshell から簡単に unfs3 を起動できると良いですね。listen localhost:9999 repeat accept unfs3 / $HOME/public-nfs/ みたいな感じで。

開発:unfs3 から GShell の内部状態を見れないと面白くないですね。まだ GoでCプログラムと動的リンクをやってみたことはありませんが…

社長:外部プログラムを起動するのに3ミリ秒かかってしまうようですし、やはり頻繁に使ったり高速な応答が必要な「外部コマンド」は動的リンクして実行する必要があると思います。

開発:考えてみればタイムスリッパでGoのgettimeofday()を簡単に入れ替えられたわけですから、あれと同じですよね。exec() をすり替えるとかですかね…明日トライしましょう。とりあえず今日はここまでで。

— 2020-0812 SatoxITS

UDPを受け入れる

開発:それでUDPを使った高速データ転送の件ですが。

社長:あれについては我社はかなり本気モードです。

基盤:公開されてるサーバへのアップロードはまだ良いとして、ダウンロードのために自分ちの玄関に穴を開けとくのは嫌ですね。

開発:そもそも受信してから、これは妙なところから来たパケットだなとかアプリケーション層で判定するのも今ひとつ。

基盤:DOS攻撃とかやられたら嫌ですね。

開発:なので、ルータレベルで、オンデマンドに特定のソースアドレスからだけを通すようにしたい。データ転送を始める前に、指定したアドレスからのUDPだけを受け取るように設定して、受信が完了したら速攻で閉じる。

基盤:iptablesをいじれば一瞬でしょうね。

社長:うちにあるルータですと、設定を変えると90秒間リブートでネットから遮断されます。

一同:(笑)

基盤:UnixでやればOSのルータ機能で簡単にできるわけです。性能的にも50MB/sの単純中継ならラズパイで足りるかもと。

開発:でもネットの口は2つ必要なんじゃないですか?

基盤:いえ、たとえばUDPは全開にして、全部ラズパイに投げる、で良いかと。

社長:開発のしやすさから言うと、ふつうにiMacでやればいいんじゃないですかね。

基盤:現状、あのへっぽこルータが生活線をささえてますから、できれば実験は別の回線にしたいですね。もうひとつ、プロバイダと契約するんですかね?

開発:今入っているV6プラスというやつ、使えるポート番号がトビトビのスカスカなのもなんですね。

基盤:4096ポートごとに15ポートですね。要するにIPマスカレードと言うかNAPTをやってるんですかね?

社長:ライトセールがV6使えないらしいのは痛いですね。ひとつくらいEC2を入れてもいいのかなと。

基盤:EC2は無料使用キャンペーンばりばりですしねw

開発:クラウドのファイアウォールではねた場合には通信料金は免除してくれそうな気もするんですよね。VMまで到達してから拒否した場合にはおそらく、課金されてしまう。

基盤:まあ、ファイアウォールの設定はブラウザでやっているわけで、HTTPのメッセージの仕様がわかれば自動でできますね。ただ、ライトセールでも数秒はかかってしまいますが。

開発:管理者の認証もしないといけないから、ブラウザで認証を通して、あとはそのブラウザに対してメッセージで指令するって感じですかね。

社長:そういえば、ブラウザ改造祭りやってたのもずいぶん昔のような気がします。

開発:まあ結局、ブラウザよりシェルのほうが断然面白いです。なにせゼロから好きなように作れますし、実装は簡単だし。

基盤:ブラウザも、Goのパッケージレベルに部品化して、shellで組み合わせればいいんじゃないですかね。

社長:うちはだいたい3日間の短期スプリントで切り替えてきましたが、GShellは1週間になりますね。

開発:ブラウザに対してちょっかいかけたり、その他もろもろ当社のインフラやアプリを統括するのもこのシェルになると思います。

社長:ついに出発点に立てた感じはしています。

— 2020-0811 SatoxITS

続々々々GShell

社長:帰りました(;゚∀゚;)ハァハァ

基盤:ただいま社内室温34.9度に達しております(;゚∀゚;)ハァハァ

開発;まあでも、扇風機の風が当たってればなんてことはないですけどね。神様がくれた気化熱に感謝です。

社長:外はもう日差しといい何といい、違う惑星に来てしまったかのようでした。

基盤:普通にあづまのゥエルシァですけどね。

開発:空気の温度を写真にとらえるのは難しいですね。

社長:それで今日はかき氷を買ってきましたよ。やわらか赤城しぐれ「いちご」じゃ~ん。

開発:すでにかなり柔らかくなってますね。食べごろと言いますか。しゃぐ。

基盤:みるみる間に解けていきますね。しゃぶ。

社長:既にかき氷を飲んでる状態になりました。じゅるじゅる…。

基盤:ただいま室温35.0度に達しました。

社長:思うに、身体の内側から冷やすのが、最も効率的なんではないかと思うのです。これ以上直接的な方法はないですよね。

開発:扇風機で気化熱方式は良いと思いますが、汗をかかないといけないので、それはそれで気持ちは良くない。なら、シャツの外側に水を垂らして蒸散させるようなやつがあると良いかと。

基盤:単にミストでも良いような。ベランダに。

社長:いや以前住んでたとこで、日光直射のベランダにある室外機が加熱しすぎて動かなくなった時、ガーデニング用のシャワーを霧状にして当てたら効果がありました。今の状況はかなり違いますが、やってみると面白そうです。今日ボーリングの試合に行くついでにホーマックで探してきます。

開発:やりすぎてお隣さんに迷惑がかからないようにしないと。

* * *

基盤:それで、先日8/2の脱エアコン声明の際に見てた気象庁のページで、この一ヶ月のデータをダウンロードしたらこういうことになってました。

開発:やはり30度を超えると世界が変わりますね。

社長:気温が下がらないので建物が冷却しないというのも問題じゃないですかね。

開発:やはり建物全体に地下水を循環させる計画ですね。

社長:ついでにミストも出しちゃう。

基盤:どっちにしろ入居率100%だから、いまいち工夫するモチベーションが無いかもですが。それで、最高だけじゃなくて最低と平均もグラフにするとこうなります。

開発:なるほど、さがるのはちゃんと下がってるんですね。

社長:確かに、朝はちゃんと涼しい感じがします。

基盤:といいますか、舘野?だかどこかの観測所の値ではなくて、当社のこの部屋の室温の変動を知る必要があります。

開発:そだね。ラズパイでやろうという計画でした。

経理:買い置きのカップ麺の蓋がみなさんぶっくりとしているのが気になります。

開発:パスカルの原理ですね。

社長:それだけ気密性が高いってことじゃないですかね。

基盤:だけど、中と外が同じ温度なら、ふくらむのは変じゃないですか?

社長:まだ賞味期限内ですし…

開発:なんでミリバールじゃなきゃいけないのかがどうも。ヘクトなんて単位よりミリのほうが正統ですよね。デシリットルだのCCだのって撲滅したくせに。

* * *

開発:さて、GShell のほうですが、たぶんリスナーを設定してやれば、やりたいことように出来ます。

社長:単にTCPのacceptまでは自分でやって、そっから先をhttpパッケージに投げるって感じにできないんですかね。ソケットを渡すとか。

開発:そうですよね。まあ、並列性の管理とかもhttpパッケージでやりたいんじゃないですかね。

社長:というか、単に自分でacceptして、リクエストのヘッダを解釈して、応答を書き出して閉じる。それだけですからね。われわれのような用途にはそれで十分です。流石にGoみたいに6行じゃ書けないですが、50行で十分に書けますよ。

開発:それはそうと、まずHTTP から受け取った文字列を、端末から受け取った文字列と同じように実行してみます。こんな感じ。

基盤:普通に実行できますね。

開発:まあ、system()関数実行しているのと大して変わらないですからね。で、問題はHTTPですし、それぞれのリクエストは単発で他とはコンテクストを共有していなということです。順序も無い。

社長:複数のユーザが並列に使う場合にはセッション管理が必須ですね。

開発:まあ、そういう使い方をするようになりましたら。各人が実際に、ttyからログインしたのと同じ状態をもたせるんでしょうね。

基盤:ユーザ側とTCPが物理的に切れてもGShellはログイン継続状態で残っているので便利ですね。永久不滅のログインみたいな。

社長:コマンドヒストリがすごいことになりそうです。

開発:CLI版の仮想マシンとして生き続けるshellは面白いと思います。

開発:それでちょっとおもしろかったのは、Chromeからやった時に、URLのバーからリクエスト文字列を入れるわけですが、途中まで入れたところで勝手にリクエストとして出したりするということです。大変よろしく無いと思います。

社長:GoにはTelnetサーバパッケージとかないんですかね?

基盤:標準のパッケージには無いですね。FTPもNFSサーバも。

開発:そのへんは、うちでufs3をGo化するとかして提供する道もありますよね。

* * *

開発:それで、HTTPサーバに制御をとられてはこまるので、これを別の並列タスクにしたいわけです。

社長:GoのスレッドってGoルーチンてやつですか。

基盤:御社の御ルーチンとか。

開発:で、どうするのかと思ったら単に、関数を呼び出す時に go というキーワードを前置きするだけで良いようです。なので早速 go という内部コマンドを足しました。

基盤:楽ちん!

社長:go を実行する前に %T を解釈してしまってますね。

開発:おっと、では%%Tではどうかな?あれ、あそうか、go と sleep と echo の3段階に解釈されるから %%%%Tでないといけない。これでどうでしょう?

基盤:正解。

社長:うーん、バックスラッシュとか%とか、何段階か解釈ムキムキされると、わけがわからなくなりますよね。なんかうまい方法は無いですかね。

開発:まあこの場合は、echo コマンド実行する時にだけ解釈しろよという指示が与えられれば、%T だけで良いはずですね。つまり、将来的に実行するコマンドについては、起動時点とか途中段階では解釈をしないとか。

社長:見た目ダサいかもですが、go { sleep 10 { echo %T } } みたいに明示的に書いたほうが良いのかも。

基盤:というか普通の感覚なら、逐次的に実行するのは go { sleep 10; echo %T } ってしますよね。

開発:いやあ、セミコロンは何かのために温存したいなという気がするんですが。だから go {{sleep 10}{`echo %T}} とか。

社長:すべてカッコだけで済ませてる言語はありますよね(^-^)。たしか閉じカッコを省略する記法がありました。例えばインタラクティブモードの時には行末で全てカッコが閉じたことにするとか。go { sleep 10 { echo %T 改行、で済ませちゃう。

開発:そもそもが、全てのコマンドが find の −exec オプション的なものを持ってたら面白いかなと思いまして。実行した結果とか環境を次のコマンドに渡すのがデフォルトと言うか。

社長:なんかすごい昔に「次に実行するコマンド」みたいな属性を見た記憶があります。まあ、環境を引き継ぐものかどうかは知りませんが。要は順次 exec() して行くみたいな。テールリカージョン的なやつ。

開発:それって、shell的なものがない時代の話じゃないですかね。とか、OSが無くて次々にオーバレイしてくみたいな。

* * *

社長:さてさてそれでは、かき氷と一緒に買ってきたメロンシャビーwをいただきますかね。

経理:結局冷蔵庫を復活させたんですね。

社長:やはり文明の利器。せいぜい月30kWhでしょう。どれどれ。ぱかっ

一同:・・・

基盤:水になってますね。

社長:冷凍庫って機能するまでに時間がかかるんですね・・・

開発:精神的ショックのせいか、変な汗が出てきました。

基盤:ああ、扇風機止まってます。

開発:なんでですかね。切タイマーとか設定してないのに・・・

基盤:全力で7Wしか食わないのにそのうえ自動で勝手に切れるとか意味がわからないですね。省電力アピールなんでしょうか?わたしはあの、洗濯機槽内照明のLEDが10秒で勝手に切れるのが非常にムカつきます。

* * *

社長:ただいま。

基盤:今日はずいぶん早く試合が終わったんですね。

社長:いえ、お盆期間で試合が休みでした。そういえば先週そんな場内アナウンスをしていたような。祝祭日と年末年始とお盆は試合は試合が休みというルールなんです。20年前の開店時から。

開発:まあ、うちは曜日とか祝日とか関係ないから忘れちゃいますよね。

基盤:夏休みだから一般客で混んでるんでしょうね。

社長:まったくガラガラでした。コロナもありますしね。まあ、昔決めたルールが意味無くなってるのに惰性というか見直す事もなく機動性も無いんでしょうね。経営者が現場を認識してないんだと思います。

開発:試合に出る人がその時期休みたいのを考慮してるのかもですが。

社長:それで、久しぶりにカスミに寄って果物を仕入れてきました。待望のスイカです。じゃーん。

基盤:おお!スイカだ。1年ぶり?

開発:やはりスーパーは入り口にある果物売り場が花ですね。ドラッグストアには真似できない。

社長:だもんで、わくわくしてスイカ、桃、プラム、メロン、パイナップル、ぶどう、トマト、とうもろこし、全部仕入れてきました。

基盤:スイカ最高ですね。あま〜い。シャグシャグ。

社長:売れ残りの値引き品ですから、熟成してるかもです。シャグ。ん、あま〜い。

開発:冷蔵庫を復活させておいてよかったですね。ぷっ。

基盤:やはり果物のある生活っていいですね。人間性回復みたいな。

社長:フルーツフルってなもんです。

開発:まあ、続けてるとすぐに当たり前になっちゃうんでしょうけどね。

— 2020-0811 SatoxITS

続々々GShell

開発:GShellにHTTPサーバ機能をもたせよう計画ですが、Goのhttpパッケージを使うとこれだけで済むことがわかりました。

開発:でこれを起動しておいて、ブラウザからアクセスするとこうなります。

社長:HTTPは基本的な部分は簡単なプロトコルですが、それにしても簡単過ぎる(笑)

開発:まあこの使い方ですとGShellがHTTPに掛かりきりになってしまいますし、端末向けに行っている処理との間で環境が共有できるのかとか、まだわからない点は多いです。

基盤:unfs3もこのくらい簡単にアドホックに使えるようになっていると良いですね。

社長:長年の宿願だった全てのアプリをHTTPサーバにする、NFSサーバにする計画の見通しは明るいですね。

開発:telnet で問いかけてみましょう。

社長:それは懐かしい…

基盤:デフォでは text/plain で答えるところがなんともシンプルで良いですね。

社長:GoのパッケージはHTTP/2.0に対応しているという話でしたっけ?

開発:問いかけてみましょう。

開発:エラーメッセージの最後に改行くらい入れろよなw

基盤:Listenerのほうで却下して、ユーザ定義のハンドラに回ってこないですね。

社長:ListenAndServeの部分をどう制御するかですね。

開発:それにしても、この暑さ、冷房は別として冷たい飲みものくらいはあってよいのでは。

社長:ぬるい麦茶で平気になっちゃいましたけどね。昔は足元に冷蔵庫があったのでペットボトルは冷やして飲んでましたが、今はちょっと冷蔵庫が遠くて。

基盤:距離6mくらいですけどね。

社長:いちおう電気代も気になりますしね。それで、ホテルに置いてあるみたいなキュービックで可愛いやつを手元に置くのは悪くないかなと。あれって昔からあこがれだったし。

基盤:あれもそこそこ、パソコン程度に電気を食うようですし、身の回りにおくと騒音とか排熱も気になるかも知れません。

開発:飲み物は別として、たまには冷えたスイカでも食べたいですね…

— 2020-810 SatoxITS

続々GShell

開発:さて、1000行あればshellぽいものになるかなと思っていたのですが、超えてしまいました。まだまだ先は遠いかなという感じです。

開発:私自身はshellをとてもシンプルにしか使わないのですが、一方でヒストリー機能とリソース使用量の記録は非常に重要だと思っています。それで、どのコマンドをどこで実行してどのくらいのリソース使用だったかというのを記録することにしました。あと、さっきいたあのディレクトリに戻りたいということが多いので、作業ディレクトリのヒストリを作ることにしました。

開発:まずディレクトリ移動のヒストリはこんな感じ。

開発:コマンドのヒストリはこんな感じです。

社長:「あのコマンドを実行したディレクトリに戻りたい」という事が多いので、cdの後の!番号もコマンドのヒストリのものを使えるのが良いように思いますね。

開発:私もそう思いました。ディレクトリ移動のヒストリは、コマンドのヒストリから抽出すれば良いのかなと。

基盤:どのディレクトリでどのくらいリソースを使う仕事をしたとか、そのディレクトリでのアクティブな滞在時間がわかると良いですね。まあそれも、コマンドのヒストリーのほうから集計すれば良いのかも知れませんが。

社長:あと、ヒストリの何番から何番までを再実行したいとか、一つのコマンド列として登録できるようになってると良いですね。

開発:ただ、長大なコマンドヒストリを探したり編集したりをラインエディタでやるのはどうかと思います。なので、そういう作業はこのgshellをHTTPサーバにして、HTTPクライアントというかブラウザからやればよいのかなと。

開発:あとこれは各コマンドの出力も後から読めるようにしたいです。デフォルトで、コマンド単位でsciriptコマンドが動いているみたいな。で、それを閲覧するのもやはり、ブラウザが適していそうです。

社長:リモートにログインした状態でそれをやるには、リモートのgshellサーバに繋がないといけないですが、ローカルな出向元のshellの口から連絡したいですよね。そういう意味でも、リモートシェル機能は内蔵していないといけない。

開発:そうですね。次の主題はこのgshellを簡単なHTTPサーバにすることだと思っています。

基盤:ヒストリの中からコピペっていうのもやりますよね。コマンド番号が邪魔だと思っていたのですが、fc -ln なんていうコマンド・オプションがある模様です。

基盤:!3-5 で !3;!4;!5 をできちゃうと良いかも。

社長:ところで「再実行」は伝統的に!ですが、?をヒストリとかの検索機能に割り当てると良いようにも思うのですが。

開発:?はファイル名の1文字のマッチングに使われてしまってますからねぇ… でもまあ、行頭の?だけは別扱いでも良いかもしません。ふつうに?として使いたかったら、./?とかすれば良さどうです。

基盤:つまり?がhistoryコマンドということですね。?3とかすると、ヒストリの3番目のコマンドをラインエディタに呼び出すとか。

社長:ディレクトリがタブでコンプリーションできると良いですね。というか、このへんはかな漢字変換のシンプル版みたいなものですから、実際かな漢字変換と同じキーバインディングで検索すればよいのかなと思いますが。

基盤:いっそIMEも作っちゃいたいですね。ローマ字かな変換までなら、簡単なプログラムで出来ると思います。

開発:あと、gshell 自体が検索したファイルとか、に対しては、find の -ls 的に表示できるようにしたいと思っています。まずはwhichでやってみました。

基盤:shared library でも同じようにできるでしょうね。

開発:ああ、あと chdir のヒストリも同様です。

開発:それとか、空白を含む引数、特にファイル名ですが、を使うためにいちいちクオートでやるのがすごく嫌です。なので、¥s を空白と解釈することにしました。

社長:空白を¥sで表現するのはDeleGateでもやってました。なんで標準的に存在してないんですかね?

開発:さあ。何にしてもこれは個人の手元で使う表記ですから、やりたいようにやれば良いかなと思います。

社長:whichでやる指定されたPATHの検索とfindは兄弟みたいなものだし、lsは find -ls の簡略版みたいものだと思います。PATHの中でファイルをワイルドカードで検索する機能はもともと動的ライブラリのために存在しますから、それをユーザがコマンドとして、あるいは引数のワイルドカードとして使えるようにすると良いと思いますね。

開発:PATHをいちいち定義するのは面倒だったりするので、これまでに滞在したディレクトリを動的にPATHとして使えれば良いですね。あるいは、これまでプログラムに渡したファイル名と思しき引数の中から検索させる、というかオートコンプリーションさせる。

社長:コンピュータの黎明期にはヒストリ機能というのはリソースを食う贅沢機能だったわけです。個人が使えるディスク容量が1メガも無い時代。コマンドシェルって、そういう時代の錯誤を引きずってる気がしますね。

基盤:まあ、ヒストリに1ギガ使ってもなんのことはないですよね。ベタ検索したって数秒もかからない。

社長:なのでこの先はずっとそういう形でヒストリを保存して、ああ10年前この時にはこんなコマンドを打ってたなみたいな思い出にひたったりするわけです。

開発:ヒストリの検索をしやすくするためには、ユーザが明示的にラベルと言うかタグを付けたいですね。というか、ヒストリをHTTPで、HTMLで眺めるという全体なら、ログインごとにタイトルとかサブタイトルを付ける必要はあるのかなと思います。ブックマークといいいますか。

社長:スクリプトにコメントを書くことは多いですが、インタラクティブに入力している端末でコメントを書くというのは、なんだかシュールで良いですね。

開発:で今気づいたんですが、bash では # 以降をコメントとして無視してくれますが、zsh はそうなってないんですね。

社長:実装上の何か困難は?

開発:だいぶGoの書き方というか、パッケージのドキュメントの流儀とか、スライスの使い方がわかって来たのでスムーズになりました。ただ今作っているのがシステム寄りのコードなのですが、os パッケージが非力というか未熟なのがつらいですかね。syscall になることが多いです。

gsh0.0.5 //Gshell by SatoxITS
// gsh - Go lang based Shell
// (c) 2020 ITS more Co., Ltd.
// 2020-0807 created by SatoxITS (sato@its-more.jp)
//
package main // gsh main
// Documents: https://golang.org/pkg/
import (
	"bufio"
	"strings"
	"strconv"
	"sort"
	"fmt"
	"os"
	"time"
	"syscall"
	"go/types"
	"go/token"
	"net"
)

var VERSION = "gsh/0.0.5 (2020-0810a)"
var LINESIZE = (8*1024)
var PATHSEP = ":" // should be ";" in Windows
var DIRSEP = "/" // canbe \ in Windows
var PROMPT = "> "
var GSH_HOME = ".gsh" // under home directory

type GCommandHistory struct {
	StartAt		time.Time
	EndAt		time.Time
	ResCode		int
	OutData		*os.File
	CmdLine		string
	ResCons		int // Resource consumption, CPU time or so
}
type GChdirHistory struct {
	Dir		string
	MovedAt		time.Time
}
type GshContext struct {
	StartDir	string	// the current directory at the start
	GetLine		string	// gsh-getline command as a input line editor
	ChdirHistory	[]GChdirHistory // the 1st entry is wd at the start
	gshPA		syscall.ProcAttr
	CommandHistory	[]GCommandHistory
	BackGround	bool
	BackGroundJobs	[]int
	LastRusage	syscall.Rusage
	GshHomeDir	string
	TerminalId	int
}

func isin(what string, list []string) bool {
	for _, v := range list  {
		if v == what {
			return true
		}
	}
	return false
}

func env(opts []string) {
	env := os.Environ()
	if isin("-s", opts){
		sort.Slice(env, func(i,j int) bool {
			return env[i] < env[j]
		})
	}
	for _, v := range env {
		fmt.Printf("%v\n",v)
	}
}

func strsubst(str string) string {
	rstr := ""
	inEsc := 0 // escape characer mode 
	for _, ch := range str {
		if inEsc == 0 {
			if ch == '\\' {
				inEsc = '\\'
				continue;
			}
		}
		if inEsc == '\\' {
			if ch == 's' { ch = ' ' }
			if ch == 'r' { ch = '\r' }
			if ch == 'n' { ch = '\n' }
			if ch == 't' { ch = '\t' }
			if ch == '\\' { ch = '\\' }
			inEsc = 0 
		}
		rstr = rstr + string(ch)
	}
	return rstr
}

func showFileInfo(path string, opts []string) {
	if isin("-ls",opts) {
		fi, _ := os.Stat(path)
		mod := fi.ModTime()
		date := mod.Format(time.Stamp)
		fmt.Printf("%v %8v %s ",fi.Mode(),fi.Size(),date)
	}
	fmt.Printf("%s",path)
	if ! isin("-n",opts) {
		fmt.Printf("\n")
	}
}

func toFullpath(path string) (fullpath string) {
	pathv := strings.Split(path,DIRSEP)
	if pathv[0] == "." {
		pathv[0], _ = os.Getwd()
	}else
	if pathv[0] == ".." {
		cwd, _ := os.Getwd()
		ppathv := strings.Split(cwd,DIRSEP)
		pathv[0] = strings.Join(ppathv,DIRSEP)
	}else
	if pathv[0] == "~" {
		pathv[0],_ = os.UserHomeDir()
	}
	return strings.Join(pathv,DIRSEP)
}

func which(list string, argv []string) (fullpathv []string, itis bool){
	if len(argv) <= 1 {
		fmt.Printf("Usage: which comand [-s] [-a] [-ls]\n")
		return []string{""}, false
	}
	path := argv[1]
	pathenv, efound := os.LookupEnv(list)
	if ! efound {
		fmt.Printf("which: no \"%s\" environment\n",list)
		return []string{""}, false
	}
	dirv := strings.Split(pathenv,PATHSEP)
	ffound := false
	ffullpath := path
	for _, dir := range dirv {
		fullpath := dir + DIRSEP + path
		fi, err := os.Stat(fullpath)
		if err != nil {
			fullpath = dir + DIRSEP + path + ".go"
			fi, err = os.Stat(fullpath)
		}
		if err == nil {
			fm := fi.Mode()
			if fm.IsRegular() {
				ffullpath = fullpath
				ffound = true
				if ! isin("-s", argv) {
					showFileInfo(fullpath,argv)
				}
				if ! isin("-a", argv) {
					break;
				}
			}
		}
	}
	return []string{ffullpath}, ffound
}

func find(argv []string){
}

func eval(argv []string, nlend bool){
	var ai = 1
	pfmt := "%s"
	if argv[ai][0:1] == "%" {
		pfmt = argv[ai]
		ai = 2
	}
	if len(argv) <= ai {
		return
	}
	gocode := strings.Join(argv[ai:]," ");
	fset := token.NewFileSet()
	rval, _ := types.Eval(fset,nil,token.NoPos,gocode)
	fmt.Printf(pfmt,rval.Value)
	if nlend { fmt.Printf("\n") }
}

func getval(name string) (found bool, val int) {
	/* should expand the name here */
	if name == "gsh.pid" {
		return true, os.Getpid()
	}else
	if name == "gsh.ppid" {
		return true, os.Getppid()
	}
	return false, 0
}

func echo(argv []string, nlend bool){
	for ai := 1; ai < len(argv); ai++ {
		if 1 < ai {
			fmt.Printf(" ");
		}
		arg := argv[ai]
		found, val := getval(arg)
		if found {
			fmt.Printf("%d",val)
		}else{
			fmt.Printf("%s",arg)
		}
	}
	if nlend {
		fmt.Printf("\n");
	}
}

func resfile() string {
	return "gsh.tmp"
}
//var resF *File
func resmap() {
	//_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, os.ModeAppend)
	// https://developpaper.com/solution-to-golang-bad-file-descriptor-problem/
	_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		fmt.Printf("refF could not open: %s\n",err)
	}else{
		fmt.Printf("refF opened\n")
	}
}

func excommand(gshCtx GshContext, exec bool, argv []string) (GshContext, bool) {
	gshPA := gshCtx.gshPA
	fullpathv, itis := which("PATH",[]string{"which",argv[0],"-s"})
	if itis == false {
		return gshCtx, true
	}
	fullpath := fullpathv[0]
	if 0 < strings.Index(fullpath,".go") {
		nargv := argv // []string{}
		gofullpathv, itis := which("PATH",[]string{"which","go","-s"})
		if itis == false {
			fmt.Printf("--F-- Go not found\n")
			return gshCtx, true
		}
		gofullpath := gofullpathv[0]
		nargv = []string{ gofullpath, "run", fullpath }
		fmt.Printf("--I-- %s {%s %s %s}\n",gofullpath,
			nargv[0],nargv[1],nargv[2])
		if exec {
			syscall.Exec(gofullpath,nargv,os.Environ())
		}else{
			pid, _ := syscall.ForkExec(gofullpath,nargv,&gshPA)
			if gshCtx.BackGround {
				fmt.Printf("--I-- in Background [%d]\n",pid)
				gshCtx.BackGroundJobs = append(gshCtx.BackGroundJobs,pid)
			}else{
				rusage := syscall.Rusage {}
				syscall.Wait4(pid,nil,0,&rusage)
				gshCtx.LastRusage = rusage
			}
		}
	}else{
		if exec {
			syscall.Exec(fullpath,argv,os.Environ())
		}else{
			pid, _ := syscall.ForkExec(fullpath,argv,&gshPA)
			//fmt.Printf("[%d]\n",pid); // '&' to be background
			if gshCtx.BackGround {
				fmt.Printf("--I-- in Background [%d]\n",pid)
				gshCtx.BackGroundJobs = append(gshCtx.BackGroundJobs,pid)
			}else{
				rusage := syscall.Rusage {}
				syscall.Wait4(pid,nil,0,&rusage);
				gshCtx.LastRusage = rusage
			}
		}
	}
	return gshCtx, false
}
func sleep(gshCtx GshContext, gshPA syscall.ProcAttr, argv []string) {
	if len(argv) < 2 {
		fmt.Printf("Sleep 100ms, 100us, 100ns, ...\n")
		return
	}
	duration := argv[1];
	d, err := time.ParseDuration(duration)
	if err != nil {
		d, err = time.ParseDuration(duration+"s")
		if err != nil {
			fmt.Printf("duration ? %s (%s)\n",duration,err)
			return
		}
	}
	fmt.Printf("Sleep %v ns\n",duration)
	time.Sleep(d)
	if 0 < len(argv[2:]) {
		gshellv(gshCtx, gshPA, argv[2:])
	}
}
func repeat(gshCtx GshContext, gshPA syscall.ProcAttr, argv []string) {
	if len(argv) < 2 {
		return
	}
	start0 := time.Now()
	for ri,_ := strconv.Atoi(argv[1]); 0 < ri; ri-- {
		if 0 < len(argv[2:]) {
			//start := time.Now()
			gshellv(gshCtx, gshPA, argv[2:])
			end := time.Now()
			elps := end.Sub(start0);
			if( 1000000000 < elps ){
				fmt.Printf("(repeat#%d %v)\n",ri,elps);
			}
		}
	}
}

func gen(gshPA syscall.ProcAttr, argv []string) {
	if len(argv) < 2 {
		fmt.Printf("Usage: %s N\n",argv[0])
		return
	}
	// should br repeated by "repeat" command
	count, _ := strconv.Atoi(argv[1])
	fd := gshPA.Files[1] // Stdout
	file := os.NewFile(fd,"internalStdOut")
	fmt.Printf("--I-- Gen. Count=%d to [%d]\n",count,file.Fd())
	//buf := []byte{}
	outdata := "0123 5678 0123 5678 0123 5678 0123 5678\r"
	for gi := 0; gi < count; gi++ {
		file.WriteString(outdata)
	}
	//file.WriteString("\n")
	fmt.Printf("\n(%d B)\n",count*len(outdata));
	//file.Close()
}

// -s, -si, -so // bi-directional, source, sync (maybe socket)
func sconnect(gshCtx GshContext, gshPA syscall.ProcAttr, inTCP bool, argv []string) {
	if len(argv) < 2 {
		fmt.Printf("Usage: -s [host]:[port[.udp]]\n")
		return
	}
	remote := argv[1]
	if remote == ":" { remote = "0.0.0.0:9999" }

	if inTCP { // TCP
		dport, err := net.ResolveTCPAddr("tcp",remote);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",remote,err)
			return
		}
		conn, err := net.DialTCP("tcp",nil,dport)
		if err != nil {
			fmt.Printf("Connection error: %s (%s)\n",remote,err)
			return
		}
		file, _ := conn.File();
		fd := file.Fd()
		fmt.Printf("Socket: connected to %s, socket[%d]\n",remote,fd)

		savfd := gshPA.Files[1]
		gshPA.Files[1] = fd;
		gshellv(gshCtx, gshPA, argv[2:])
		gshPA.Files[1] = savfd
		file.Close()
		conn.Close()
	}else{
		//dport, err := net.ResolveUDPAddr("udp4",remote);
		dport, err := net.ResolveUDPAddr("udp",remote);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",remote,err)
			return
		}
		//conn, err := net.DialUDP("udp4",nil,dport)
		conn, err := net.DialUDP("udp",nil,dport)
		if err != nil {
			fmt.Printf("Connection error: %s (%s)\n",remote,err)
			return
		}
		file, _ := conn.File();
		fd := file.Fd()

		ar := conn.RemoteAddr()
		//al := conn.LocalAddr()
		fmt.Printf("Socket: connected to %s [%s], socket[%d]\n",
			remote,ar.String(),fd)

		savfd := gshPA.Files[1]
		gshPA.Files[1] = fd;
		gshellv(gshCtx, gshPA, argv[2:])
		gshPA.Files[1] = savfd
		file.Close()
		conn.Close()
	}
}
func saccept(gshCtx GshContext, gshPA syscall.ProcAttr, inTCP bool, argv []string) {
	if len(argv) < 2 {
		fmt.Printf("Usage: -ac [host]:[port[.udp]]\n")
		return
	}
	local := argv[1]
	if local == ":" { local = "0.0.0.0:9999" }
	if inTCP { // TCP
		port, err := net.ResolveTCPAddr("tcp",local);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",local,err)
			return
		}
		//fmt.Printf("Listen at %s...\n",local);
		sconn, err := net.ListenTCP("tcp", port)
		if err != nil {
			fmt.Printf("Listen error: %s (%s)\n",local,err)
			return
		}
		//fmt.Printf("Accepting at %s...\n",local);
		aconn, err := sconn.AcceptTCP()
		if err != nil {
			fmt.Printf("Accept error: %s (%s)\n",local,err)
			return
		}
		file, _ := aconn.File()
		fd := file.Fd()
		fmt.Printf("Accepted TCP at %s [%d]\n",local,fd)

		savfd := gshPA.Files[0]
		gshPA.Files[0] = fd;
		gshellv(gshCtx, gshPA, argv[2:])
		gshPA.Files[0] = savfd

		sconn.Close();
		aconn.Close();
		file.Close();
	}else{
		//port, err := net.ResolveUDPAddr("udp4",local);
		port, err := net.ResolveUDPAddr("udp",local);
		if err != nil {
			fmt.Printf("Address error: %s (%s)\n",local,err)
			return
		}
		fmt.Printf("Listen UDP at %s...\n",local);
		//uconn, err := net.ListenUDP("udp4", port)
		uconn, err := net.ListenUDP("udp", port)
		if err != nil {
			fmt.Printf("Listen error: %s (%s)\n",local,err)
			return
		}
		file, _ := uconn.File()
		fd := file.Fd()
		ar := uconn.RemoteAddr()
		remote := ""
		if ar != nil { remote = ar.String() }
		if remote == "" { remote = "?" }

		// not yet received
		//fmt.Printf("Accepted at %s [%d] <- %s\n",local,fd,"")

		savfd := gshPA.Files[0]
		gshPA.Files[0] = fd;
		savenv := gshPA.Env
		gshPA.Env = append(savenv, "REMOTE_HOST="+remote)
		gshellv(gshCtx, gshPA, argv[2:])
		gshPA.Env = savenv
		gshPA.Files[0] = savfd

		uconn.Close();
		file.Close();
	}
}

// empty line command
func pwd(gshPA syscall.ProcAttr){
	// execute context command, pwd + date
	// context notation, representation scheme, to be resumed at re-login
	cwd, _ := os.Getwd()
	t := time.Now()
	date := t.Format(time.UnixDate)
	exe, _ := os.Executable()
	host, _ := os.Hostname()
	fmt.Printf("{PWD=\"%s\"",cwd)
	fmt.Printf(" HOST=\"%s\"",host)
	fmt.Printf(" DATE=\"%s\"",date)
	fmt.Printf(" TIME=\"%s\"",t.String())
	fmt.Printf(" PID=\"%d\"",os.Getpid())
	fmt.Printf(" EXE=\"%s\"",exe)
	fmt.Printf("}\n")
}

// these should be browsed and edited by HTTP browser
// show the time of command with -t and direcotry with -ls
// openfile-history, sort by -a -m -c
// sort by elapsed time by -t -s
// search by "more" like interface
// edit history
// sort history, and wc or uniq
// CPU and other resource consumptions
// limit showing range (by time or so)
// export / import history
func xHistory(gshCtx GshContext, argv []string) (rgshCtx GshContext) {
	for i, v := range gshCtx.CommandHistory {
		fmt.Printf("!%d ",i)
		if isin("-v",argv){
			fmt.Println(v) // should be with it date
		}else{
			if isin("-l",argv){
				elps := v.EndAt.Sub(v.StartAt);
				start := v.StartAt.Format(time.Stamp)
				fmt.Printf("%s (%8v) ",start,elps)
			}
			fmt.Printf("%s",v.CmdLine)
			fmt.Printf("\n")
		}
	}
	return gshCtx
}
// !n - history index
func searchHistory(gshCtx GshContext, gline string) (string, bool, bool){
	if gline[0] == '!' {
		hix, err := strconv.Atoi(gline[1:])
		if err != nil {
			fmt.Printf("--E-- (%s : range)\n",hix)
			return "", false, true
		}
		if hix < 0 || len(gshCtx.CommandHistory) <= hix {
			fmt.Printf("--E-- (%d : out of range)\n",hix)
			return "", false, true
		}
		return gshCtx.CommandHistory[hix].CmdLine, false, false
	}
	// search
	//for i, v := range gshCtx.CommandHistory {
	//}
	return gline, false, false
}

// temporary adding to PATH environment
// cd name -lib for LD_LIBRARY_PATH
// chdir with directory history (date + full-path)
// -s for sort option (by visit date or so)
func xChdirHistory(gshCtx GshContext, argv []string){
	for i, v := range gshCtx.ChdirHistory {
		fmt.Printf("!%d ",i)
		fmt.Printf("%v ",v.MovedAt.Format(time.Stamp))
		showFileInfo(v.Dir,argv)
	}
}
func xChdir(gshCtx GshContext, argv []string) (rgshCtx GshContext) {
	cdhist := gshCtx.ChdirHistory
	if isin("?",argv ) || isin("-t",argv) {
		xChdirHistory(gshCtx,argv)
		return gshCtx
	}
	pwd, _ := os.Getwd()
	dir := ""
	if len(argv) <= 1 {
		dir = toFullpath("~")
	}else{
		dir = argv[1]
	}
	if dir[0] == '!' {
		if dir == "!0" {
			dir = gshCtx.StartDir
		}else
		if dir == "!!" {
			index := len(cdhist) - 1
			if 0 < index { index -= 1 }
			dir = cdhist[index].Dir
		}else{
			index, err := strconv.Atoi(dir[1:])
			if err != nil {
				fmt.Printf("--E-- xChdir(%v)\n",err)
				dir = "?"
			}else
			if len(gshCtx.ChdirHistory) <= index {
				fmt.Printf("--E-- xChdir(history range error)\n")
				dir = "?"
			}else{
				dir = cdhist[index].Dir
			}
		}
	}
	if dir != "?" {
		err := os.Chdir(dir)
		if err != nil {
			fmt.Printf("--E-- xChdir(%s)(%v)\n",argv[1],err)
		}else{
			cwd, _ := os.Getwd()
			if cwd != pwd {
				hist1 := GChdirHistory { }
				hist1.Dir = cwd
				hist1.MovedAt = time.Now()
				gshCtx.ChdirHistory = append(cdhist,hist1)
			}
		}
	}
	return gshCtx
}
func showRusage(what string,argv []string, ru *syscall.Rusage){
	fmt.Printf("%s: ",what);
	fmt.Printf("Usr=%d.%06ds",ru.Utime.Sec,ru.Utime.Usec)
	fmt.Printf(" Sys=%d.%06ds",ru.Stime.Sec,ru.Stime.Usec)
	fmt.Printf(" Rss=%vB",ru.Maxrss)
	if isin("-l",argv) {
		fmt.Printf(" MinFlt=%v",ru.Minflt)
		fmt.Printf(" MajFlt=%v",ru.Majflt)
		fmt.Printf(" IxRSS=%vB",ru.Ixrss)
		fmt.Printf(" IdRSS=%vB",ru.Idrss)
		fmt.Printf(" Nswap=%vB",ru.Nswap)
	fmt.Printf(" Read=%v",ru.Inblock)
	fmt.Printf(" Write=%v",ru.Oublock)
	}
	fmt.Printf(" Snd=%v",ru.Msgsnd)
	fmt.Printf(" Rcv=%v",ru.Msgrcv)
	//if isin("-l",argv) {
		fmt.Printf(" Sig=%v",ru.Nsignals)
	//}
	fmt.Printf("\n");
}
func xTime(gshCtx GshContext, argv []string) (GshContext,bool) {
	if 2 <= len(argv){
		xgshCtx, fin := gshellv(gshCtx,gshCtx.gshPA,argv[1:])
		gshCtx = xgshCtx
		showRusage(argv[1],argv,&gshCtx.LastRusage)
		return gshCtx, fin
	}else{
		rusage:= syscall.Rusage {}
		syscall.Getrusage(syscall.RUSAGE_SELF,&rusage)
		showRusage("self",argv, &rusage)
		syscall.Getrusage(syscall.RUSAGE_CHILDREN,&rusage)
		showRusage("child",argv, &rusage)
		return gshCtx, false
	}
}
func xJobs(gshCtx GshContext, argv []string){
	fmt.Printf("%d Jobs\n",len(gshCtx.BackGroundJobs))
	for ji, pid := range gshCtx.BackGroundJobs {
		//wstat := syscall.WaitStatus {0}
		rusage := syscall.Rusage {}
		//wpid, err := syscall.Wait4(pid,&wstat,syscall.WNOHANG,&rusage);
		wpid, err := syscall.Wait4(pid,nil,syscall.WNOHANG,&rusage);
		if err != nil {
			fmt.Printf("--E-- %%%d [%d] (%v)\n",ji,pid,err)
		}else{
				fmt.Printf("%%%d[%d](%d)\n",ji,pid,wpid)
			showRusage("chld",argv,&rusage)
		}
	}
}

func gshellv(gshCtx GshContext, gshPA syscall.ProcAttr, argv []string) (xx GshContext, fin bool) {
	//fmt.Printf("--I-- gshellv((%d))\n",len(argv))
	if len(argv) <= 0 {
		return gshCtx, false
	}
	for ai := 0; ai < len(argv); ai++ {
		argv[ai] = strsubst(argv[ai])
	}
	if false {
		for ai := 0; ai < len(argv); ai++ {
			fmt.Printf("[%d] %s [%d]%T\n",
				ai,argv[ai],len(argv[ai]),argv[ai])
		}
	}
	cmd := argv[0]
	if cmd == "-ot" {
		sconnect(gshCtx, gshPA, true, argv)
		return gshCtx, false
	}
	if cmd == "-ou" {
		sconnect(gshCtx, gshPA, false, argv)
		return gshCtx, false
	}
	if cmd == "-it" {
		saccept(gshCtx, gshPA, true , argv)
		return gshCtx, false
	}
	if cmd == "-iu" {
		saccept(gshCtx, gshPA, false, argv)
		return gshCtx, false
	}
	if cmd == "-i" || cmd == "-o" || cmd == "-a" || cmd == "-s" {
		if len(argv) < 2 {
			return gshCtx, false
		}
		fdix := 0;
		mode := os.O_RDONLY;
		if cmd == "-i" {
		}
		if cmd == "-o" {
			fdix = 1;
			mode = os.O_RDWR | os.O_CREATE;
		}
		if cmd == "-a" {
			fdix = 1;
			mode = os.O_RDWR | os.O_CREATE | os.O_APPEND;
		}
		f, err := os.OpenFile(argv[1], mode, 0600)
		if err != nil {
			fmt.Printf("%s\n",err)
			return gshCtx, false
		}
		savfd := gshPA.Files[fdix]
		gshPA.Files[fdix] = f.Fd()
		fmt.Printf("--I-- Opened [%d] %s\n",f.Fd(),argv[1])
		gshCtx, _ = gshellv(gshCtx, gshPA, argv[2:])
		gshPA.Files[fdix] = savfd
		return gshCtx, false
	}
	if cmd == "-bg" {
		xfin := false
		// set background option
		gshCtx.BackGround = true
		gshCtx, xfin = gshellv(gshCtx,gshPA,argv[1:])
		gshCtx.BackGround = false
		return gshCtx, xfin
	}
	if cmd == "call" {
		gshCtx, _ = excommand(gshCtx, false,argv[1:])
		return gshCtx, false
	}
	if cmd == "cd" || cmd == "chdir" {
		gshCtx = xChdir(gshCtx,argv);
		return gshCtx, false
	}
	if cmd == "#define" {
	}
	if cmd == "echo" {
		echo(argv,true)
		return gshCtx, false
	}
	if cmd == "env" {
		env(argv)
		return gshCtx, false
	}
	if cmd == "eval" {
		eval(argv,true)
		return gshCtx, false
	}
	if cmd == "exec" {
		gshCtx, _ = excommand(gshCtx, true,argv[1:])
		// should not return here
		return gshCtx, false
	}
	if cmd == "exit" || cmd == "quit" {
		// write Result code EXIT to 3>
		return gshCtx, true
	}
	if cmd == "-find" {
		find(argv)
		return gshCtx, false
	}
	if cmd == "fork" {
		// mainly for a server
		return gshCtx, false
	}
	if cmd == "-gen" {
		gen(gshPA, argv)
		return gshCtx, false
	}
	if cmd == "history" || cmd == "hi" { // hi should be alias
		gshCtx = xHistory(gshCtx, argv)
		return gshCtx, false
	}
	if cmd == "jobs" {
		xJobs(gshCtx,argv)
		return gshCtx, false
	}
	if cmd == "nop" {
		return gshCtx, false
	}
	if cmd == "pstitle" {
		// to be gsh.title
	}
	if cmd == "repeat" || cmd == "rep" { // repeat cond command
		repeat(gshCtx,gshPA,argv)
		return gshCtx, false
	}
	if cmd == "set" { // set name ...
		return gshCtx, false
	}
	if cmd == "time" {
		gshCtx, fin = xTime(gshCtx,argv)
		return gshCtx, fin
	}
	if cmd == "sleep" {
		sleep(gshCtx,gshPA,argv)
		return gshCtx, false
	}
	if cmd == "-ver" {
		fmt.Printf("%s\n",VERSION);
		return gshCtx, false
	}
	if cmd == "pwh" {
		pwd(gshPA);
		return gshCtx, false
	}
	if cmd == "where" {
		// data file or so?
	}
	if cmd == "which" {
		which("PATH",argv);
		return gshCtx, false
	}
	gshCtx, _ = excommand(gshCtx, false,argv)
	return gshCtx, false
}
func gshelll(gshCtx GshContext, gshPA syscall.ProcAttr, gline string) (gx GshContext, rfin bool) {
	argv := strings.Split(string(gline)," ")
	gshCtx, fin := gshellv(gshCtx,gshPA,argv)
	return gshCtx, fin
}
func tgshelll(gshCtx GshContext, gshPA syscall.ProcAttr, gline string) (gx GshContext, xfin bool) {
	start := time.Now()
	gshCtx, fin := gshelll(gshCtx,gshPA,gline)
	end := time.Now()
	elps := end.Sub(start);
	fmt.Printf("--I-- (%d.%09ds)\n",elps/1000000000,elps%1000000000)
	return gshCtx, fin
}
func Ttyid() (int) {
	fi, err := os.Stdin.Stat()
	if err != nil {
		return 0;
	}
	//fmt.Printf("Stdin: %v Dev=%d\n",
	//	fi.Mode(),fi.Mode()&os.ModeDevice)
	if (fi.Mode() & os.ModeDevice) != 0 {
		stat := syscall.Stat_t{};
		err := syscall.Fstat(0,&stat)
		if err != nil {
			//fmt.Printf("--I-- Stdin: (%v)\n",err)
		}else{
			//fmt.Printf("--I-- Stdin: rdev=%d %d\n",
			//	stat.Rdev&0xFF,stat.Rdev);
			//fmt.Printf("--I-- Stdin: tty%d\n",stat.Rdev&0xFF);
			return int(stat.Rdev & 0xFF)
		}
	}
	return 0
}
func ttyfile(gshCtx GshContext) string {
	//fmt.Printf("--I-- GSH_HOME=%s\n",gshCtx.GshHomeDir)
	ttyfile := gshCtx.GshHomeDir + "/" + "gsh-tty" +
		 strconv.Itoa(gshCtx.TerminalId)
	//fmt.Printf("--I-- ttyfile=%s\n",ttyfile)
	return ttyfile
}
func ttyline(gshCtx GshContext) (*os.File){
	file, err := os.OpenFile(ttyfile(gshCtx),
		os.O_RDWR|os.O_CREATE|os.O_TRUNC,0600)
	if err != nil {
		fmt.Printf("--F-- cannot open %s (%s)\n",ttyfile(gshCtx),err)
		return file;
	}
	return file
}
func getline(gshCtx GshContext, hix int, skipping, with_exgetline bool, gsh_getlinev[]string, prevline string) (string) {
	if( skipping ){
		reader := bufio.NewReaderSize(os.Stdin,LINESIZE)
		line, _, _ := reader.ReadLine()
		return string(line)
	}else
	if( with_exgetline && gshCtx.GetLine != "" ){
		//var xhix int64 = int64(hix); // cast
		newenv := os.Environ()
		newenv = append(newenv, "GSH_LINENO="+strconv.FormatInt(int64(hix),10) )

		tty := ttyline(gshCtx)
		tty.WriteString(prevline)
		Pa := os.ProcAttr {
			"", // start dir
			newenv, //os.Environ(),
			[]*os.File{os.Stdin,os.Stdout,os.Stderr,tty},
			nil,
		}
//fmt.Printf("--I-- getline=%s // %s\n",gsh_getlinev[0],gshCtx.GetLine)
proc, err := os.StartProcess(gsh_getlinev[0],[]string{"getline","getline"},&Pa)
		if err != nil {
			fmt.Printf("Proc ERROR (%s)\n",nil)
			for ; ; {
			}
		}
		//stat, err := proc.Wait()
		proc.Wait()
		buff := make([]byte,LINESIZE)
		count, err := tty.Read(buff)
		//_, err = tty.Read(buff)
		//fmt.Printf("--D-- getline (%d)\n",count)
		if err != nil {
			if ! (count == 0) { // && err.String() == "EOF" ) {
				fmt.Printf("--E-- getline error (%s)\n",err)
			}
		}else{
			//fmt.Printf("--I-- getline OK \"%s\"\n",buff)
		}
		tty.Close()
		return string(buff[0:count])
	}else{
		// if isatty {
			fmt.Printf("!%d",hix)
			fmt.Print(PROMPT)
		// }
		reader := bufio.NewReaderSize(os.Stdin,LINESIZE)
		line, _, _ := reader.ReadLine()
		return string(line)
	}
}
//
// $USERHOME/.gsh/
//                gsh-history.txt
//                gsh-aliases.txt // should be conditional?
//
func gshSetup(gshCtx GshContext) (GshContext, bool) {
	homedir, err := os.UserHomeDir()
	if err != nil {
		fmt.Printf("--E-- You have no UserHomeDir (%v)\n",err)
		return gshCtx, true
	}
	gshhome := homedir + "/" + GSH_HOME
	_, err2 := os.Stat(gshhome)
	if err2 != nil {
		err3 := os.Mkdir(gshhome,0700)
		if err3 != nil {
			fmt.Printf("--E-- Could not Create %s (%s)\n",
				gshhome,err)
			return gshCtx, true
		}
		fmt.Printf("--I-- Created %s\n",gshhome)
	}
	gshCtx.GshHomeDir = gshhome
	return gshCtx, false
}
func script(gshCtx GshContext) (_ GshContext) {
	gshCtx, err0 := gshSetup(gshCtx)
	if err0 {
		return gshCtx
	}
	gshPA := syscall.ProcAttr {
		"", // the staring directory
		os.Environ(), // environ[]
		[]uintptr{os.Stdin.Fd(),os.Stdout.Fd(),os.Stderr.Fd()},
		nil, // OS specific
	}
	cwd, _ := os.Getwd()
	gshCtx = GshContext {
		cwd, // StartDir
		"", // GetLine
		[]GChdirHistory { {cwd,time.Now()} }, // ChdirHistory
		gshPA,
		[]GCommandHistory{ }, //something for invokation?
		false,
		[]int{},
		syscall.Rusage{},
		"", // GshHomeDir
		Ttyid(),
	}
	gshCtx, _ = gshSetup(gshCtx)
	fmt.Printf("--I-- GSH_HOME=%s\n",gshCtx.GshHomeDir)
	//resmap()
	gsh_getlinev, with_exgetline :=
		 which("PATH",[]string{"which","gsh-getline","-s"})
	if with_exgetline {
		gsh_getlinev[0] = toFullpath(gsh_getlinev[0])
		gshCtx.GetLine = toFullpath(gsh_getlinev[0])
	}else{
	fmt.Printf("--W-- No gsh-getline found. Using internal getline.\n");
	}

	prevline := ""
	skipping := false
	for hix := 1; ; {
		gline := getline(gshCtx,hix,skipping,with_exgetline,gsh_getlinev,prevline)
		if skipping {
			if strings.Index(gline,"fi") == 0 {
				fmt.Printf("fi\n");
				skipping = false;
			}else{
				//fmt.Printf("%s\n",gline);
			}
			continue
		}
		if strings.Index(gline,"if") == 0 {
			//fmt.Printf("--D-- if start: %s\n",gline);
			skipping = true;
			continue
		}
		if 0 < len(gline) && gline[0] == '!' {
			xgline, set, err := searchHistory(gshCtx,gline)
			if err {
				continue
			}
			if set {
				// set the line in command line editor
			}
			gline = xgline
		}
		ghist := GCommandHistory { }
		ghist.StartAt = time.Now()
		xgshCtx, fin := tgshelll(gshCtx,gshPA,gline)
		gshCtx = xgshCtx
		ghist.EndAt = time.Now()
		ghist.CmdLine = gline
		gshCtx.CommandHistory = append(gshCtx.CommandHistory, ghist)
		if fin {
			break;
		}
		if len(gline) == 0 {
			pwd(gshPA);
			continue;
		}
		prevline = gline;
		hix++;
	}
	return gshCtx
}
func main() {
	gshPA := syscall.ProcAttr {
		"", // the staring directory
		os.Environ(), // environ[]
		[]uintptr{},
		nil, // OS specific
	}
	gshCtx := GshContext {
		"", // StartDir
		"", // GetLine
		[]GChdirHistory{ {"",time.Now()} },
		gshPA,
		[]GCommandHistory{ },
		false,
		[]int{},
		syscall.Rusage{},
		"", // GshHomeDir
		Ttyid(),
	}
	gshCtx = script(gshCtx)
	fmt.Printf("%s\n",gshCtx.StartDir)
}
// TODO:
// - inter gsh communication, possibly running in remote hosts -- to be remote shell
// - merged histories of multiple parallel gsh sessions
// - alias as a function
// - instant alias end environ export to the permanent > ~/.gsh/gsh-alias and gsh-environ
// - retrieval PATH of files by its type
// - gsh as an IME
// - all commands have its subucomand after "---" symbol
// - filename expansion by "-find" command
// - history of ext code and output of each commoand
//---END--- (^-^)/

— 2020-0810 SatoxITS

続Gshell

開発:さて、UDPでデータ転送の件は一段落したので、gshellに戻りたいとおもいます。

基盤:というかさっきから扇風機の風が生暖かいと思ったら、室内が32.6度に達しています。

社長:この扇風機がなかったら我々は終わっていますね。ありがたいことです。

経理:3万円の扇風機… 100日間使って一日300円、1000日使っても一日30円。

基盤:10年は使わないと割にあわないですね。

社長:エアコンを平均500W一日24時間30日回し続けたら、360kWh。25円/kWhだとすれば9,000円/月です。実際以前夏場はそれくらいかそれ以上の料金上積みでした。まあそれでも、飲み代2回分より安いわけで、気にしてなかったんです。

経理:会社の経費の基準は飲み代ではないですからね。

基盤:今はアマゾンセール$5がコスト比較原器です。

開発:最近はでんこ家計簿が超お楽しみコンテンツになりましたが、もう200W程度で下限で安定してしまっていて面白くなくなりました。

社長:200Wって、ちょっとしたソーラーパネルでできちゃいますよね。日中はそれでいけちゃいそうな気もしますが。

基盤:アマゾンで見ると、パネル自体は100Wで1万円というところですね。大きさは1m x 50cm くらいので扱いやすいかなと。

社長:うーん、ここはなまじヒサシが深いですからねぇ。屋上におかせてもらえると良いのですが…

開発:でも、元が取れるとかどうかではなくて、遊びとしては面白いですよね。そのためにはベランダに置いたほうが良いと思います。

社長:一応災害時への備えとかにもなりますしね。

* * *

開発:まずは gsh の現状版。v0.0.4です。

開発:現在Goで574行。

基盤:Cで書いている部分が700行を超えてますが。

開発:それはshell自体の機能というより個別の外部コマンドです。あと、システムコール周りとか、Cなら簡単に書ける部分も多い。

社長:全部Goで書いたほうが移植性は良いと思いますが。

開発:我々はUnixとWindows以外への移植性を考える必要は無いと思います。DeleGateはCで書かれましたが、Windows でも LinuxでもMacOSでもBSDでも、どこでも動きますよね。使っている外部のライブラリはOpenSSLとZlibだけで、しかも動的リンクのオプショナル。問題ないと思います。

社長:まあ、Cプログラムのビルド環境がデフォでは無い場合もあるし、バイナリ配布が必要になりますね。

開発:DLLで配れば良いのかなと思います。

基盤:しかし、コマンドライン入力部分をCで書いているのはユニークかなと。

開発:tty の処理はもう何十年前からCで書いてなれてますし。要はttyというプロセスとgshellで通信するのが、端末も遠隔に出来て良いのではないかと思うわけです。

社長:ttyとネットワークで繋がるということは、要はリモートシェルになるわけですね。

開発:リモートであるとかローカルであるとかは見えなくなると思います。あるいは、一つの端末を複数のshellの間でattach/detachする。gshell自体はずっとマシンの中で生き続けていて、それにつなぐだけです。ログインに時間がかるような事もありません。

社長:ずっと状態が残っているのは便利ですね。

基盤:一種の仮想マシンのような状態なわけですね。

開発:gshellというのは基本は永続的なサービスとして生きていて、thin client的なクライアント、例えばtelnetから接続する感じかなと思います。

社長:複数同時に動いているgshellの間のネットワークを作って、横方向にも協調させると良いですね。それ自体が、アプリケーションレベルのVPNのような世界を作る。

開発:あとはファイル空間ですね。仲間のgshellが見ているファイルシステムを他のgshellでも見える。これは、NFSをgshellのネットワークで中継すれば良いかなと思います。実際、何でもできちゃうログインをしたいわけではなくて、単にそのホストにあるファイルをみたいだけということが多いです。だから、ログイン的なセッションを張っている間だけ、互いのファイルがマウントされて見えるようなのが良いかなと思いますね。便利だし、安全性の面でも。まあ要するにプライベートな automount 的な。

基盤:unfs3に出会う前は、open/read/write/close といったファイル関係のシステムコールを動的ライブラリで置き換えるって話でしたよね。

開発:その線も捨ててはいません。適材適所かなと。

社長:Gshellにtelnetで接続する様を是非見てみたいです。それ、最優先で。

基盤:macOSってtelnet無いんですよね。インストールしないと… えっと… brew install telnet。15秒でインストール完了です。

開発:ええええええええ!ずっと不便だと思いながら耐えてきたのに…

会社:もう身体にしみついちゃってるから、無意識に telnet って打っちゃいますよね (笑)

* * *

開発:とこで、フランクフルトにsshでログインするのに4秒以上かかるのがすごいストレスなんですが。

基盤:まあ汎用にできてるでしょうから色々ネゴしているでしょうね。1ネゴに0.5秒掛かるわけで。

社長:じゃーん。そういう時のために、私が昔作ったリモートシェルサーバはいかがでしょうか?実は昔の名残で、全ライトセール機でサーバ自体は動いているのです。あとはファイアーウォールを通してもらえれば。

基盤:ポート番号は?

社長:ごにょごにょ…

基盤:では、我社のIPアドレスからだけ、TCPを通します…

開発:しかしライトセールって、日本語のわかるスタッフ雇ってないんでしょうね。ここでもてにをはがおかしい。

社長:なにせ安くあげるのが至上命題でしょうからね。我々もその恩恵を受けている。サービス提供では人件費が一番金を食う。

基盤:はい、全ライトセール機のファイアウォールを設定しました。

社長:それでは、さっそく。えーと使い方は、ああ、単にホスト名を指定すれば良いのでした。ではログイン。

基盤:なんかさくっと入りましたね。

社長:まあ、入った後は、キー入力のエコーとかは0.5秒かかるのはどうにもならないですが。RTT 0.25秒ですから。

開発:ちょっとログインの所要時間を測ってみましょう。

開発:4秒は耐え難いですが、1秒はまあありですね。1秒ということは、認証に2往復しているんですね?

社長:yysh に -fv オプションを付けると、HTTP風なメッセージのやりとりが見えますよ。まあ繋がると同時に両側から情報を出せばもう一段高速化はするかと思います。

基盤:普通に使えるんですか?

社長:昔はsshサーバとかほとんど無かったし、これで何年も暮らしてました。X Windowも中継しますよ。何段でも多段中継して。

基盤:TCPコネクションは一本なんですか?

社長:そうです。YYMUXという自作のマルチプレクサーです。

開発:暗号化は?

社長:鍵交換は自作のDiffie-Hellmanです。当時はOpenSSLがまだ広くは普及してませんでしたしね。というかSSLeayでしたけど。暗号化はCredhyというこれも自作のストリーム暗号アルゴリズム(^-^)

開発:鉄壁ですねw

社長:では思い出しがてら、デモをば… ドイツ支所に居るxeyesでつくば本社内を監視させてみます。

フランクフルトからつくば本社内を監視するxeyes

基盤:おおぅ。一応カクカク追随しますね。

社長:まあ、RTTが250msなもので。

開発:なんにしても長距離でキーのエコーとか遅延がどうにもならないですから、エディタ的なインタラクティブなものは手元で動かして、ファイルシステムだけ遠方のをマウントするという事になるでしょうね。

社長:せっかくなので、我社世界5拠点を数珠つなぎに一周してみたいですね。

基盤:では各サイトのファイアウォールをそのように設定…

社長:では、東京>オレゴン>フランクフルト>ムンバイ>シドニーを一巡りしてみます。えい。

基盤:3秒で帰ってきました。なかなかですね。

開発:これって、通過したところでそれぞれにトンネルを作りますよね。一本のシリアルバス状のトンネルを作ると、通過した地点の間のVPN的なものができると便利かなと思います。

基盤:それって乗り合いバスというより、乗り合いタクシーみたいですね。

社長:面白いけど、隣以外と通信する時にいちいち多段中継されるのでは、遅延が厳しい気はしますね。何かスター状のバスもはらないと…

開発:たぶん、リフレクタ的なバスをどこかに作って、そこに対するアクセス許可をシリアルバスで順次申し伝えするって感じですかね。

社長:アクセス権の面でも、そいつがどこからどうやって来た馬の骨かがわかると、面白い管理ができるかも知れません。

開発:ドメイン名システムができる前のUUCPのパスアドレスを思い出します。

社長:ドメイン名が今みたいな商売になるとは当時は全く想像しなかったですね。

開発:そもそもインターネット、IPがこんな風に一般社会の基盤になるとは。

社長:まあ、なるといいなとは思ってましたけどね。

— 2020-0809 SatoxITS

長距離UDPで驚速データ転送を実現

開発:そもそもshellを作ろうと思い立ったのは昨日の朝、UDPでならどの程度のデータ転送速度が実現できるか興味を持ったためです。でついつい、Cでゼロからプログラムを書き始めてしまったのですが、しばらくして、いやいやこんな力仕事的な書き方はもう卒業すべきだと思い、Goに立ち返ったのでした。

開発:で、ソケットを作ったり繋いだりするのはGoのパッケージを使えば簡単に書けるわけです。まあ、ライブラリがあればどんな言語だって簡単ですけど。しかも、Goで使うべき関数を間違えてしばらく戸惑いました。システム関係のパッケージのドキュメントはどうも完成度が低い気がします。まあともかく、やってみた結果がこれです。

基盤:2.27GB/s!

開発:もちろんUDPですから、データは取りこぼすわけです。ですが、同一マシン内で、マシンの負荷が軽ければ、ほぼ受け取れます。この例では 99.7%は受け取れています。

基盤:このMacMiniがこれまで生きてきて一番高速なネットワーク転送です。

社長:使用しているメモリ2.7GHzですからね。まあおそらく64bit、8バイト/クロックでアクセスできるんでしょうけど。いや、32ビットかな?

開発:32ビット4バイトだったとすると4 x 2.7 で 10.8GB/s、64ビット8バイトだとすると21.6GB/s は出るはずですね。

基盤:172.8Gbps。

社長:メモリのコピーでは読んで書くから、その半分になる。だからメモリのコピーは最速で10GB/sと思います。

開発:ただ、CPUの命令ループでコピーする場合、1ワードのコピーにCPUクロックが複数かかりますね。ソースとディスティネーションのポインタレジスタの更新、終了判定、あとはロードとストアと、結局8バイトのコピーに8クロックくらいかかると思うんです。簡単なテストプログラムを書いて測りました。volatile にした 100MBのバッファの間をでコピーを10回繰り返すというものです。

基盤:理論値の10GB/s出てますね。

社長:ユーザからDMAは使えないんですかね?

開発:さあ。このmemcpyの値は、異常に速かったり遅かったりします。デュアルポートメモリでも無いでしょうに18GB/s出ているのも謎です。他のCPUループでの実装はほぼ安定していますので、ひょっとしてDMAを使っているのかも知れません。デバイスとして使えるんでしょうか。システムコールで叩いてるんでしょうか?

基盤:通信経由のコピーがプロセス内でのメモリーメモリコピーと同等というのはびっくりですね。

開発:それも実際にはDMAによるメモリーメモリコピーに帰着させてるんじゃないかとは思いますけどね。他のマシンへの転送では、数マイクロ秒間隔を開けてやらないと、OSのバッファがいっぱいになってエラーが出るのですが、同一マシン内だとそれが出ないんです。

社長:自分自身に送信してコピーすると高速化したりするかもですねw

* * *

開発:それで、次はTCPではどうなのかといことになります。やってみた結果がこれ。

基盤:ネットが6.23Gbpsに到達しております!

開発:このMacMiniは、外に対しては1GB/sでしか繋がりませんが、内部的には違う話ですね。プロセス間での通信は、プロセス内部でのメモリのコピーと同等の性能が出ていると見えます。

社長:TCPのほうが速いんですね。

開発:UDPだと固定長パケットで、8KBを超えると起こられてしまうわけです。だから送信の回数、OSとの通信が多くなります。一方TCPではそういう制約は無いですから、でっかいバッファのありかをどーんとOSに教えてやって任せればよいわけです。200KB程度のサイズまでは、サイズが大きいほど速くなります。

基盤:ただこれは、同一マシン内での通信の特殊状況なんでしょうね。

開発:そうですね。ping への RTT が 0.05ms とかいう世界です。これが、隣のマシンになると 0.5ms程度はどうしてもかかる。さらにWANに出ると速くても5ms程度はかかる。アジア近隣で50ms、アメリカまで行くと130ms、ヨーロッパまで行くと250msになってしまうわけです。相手の受信確認を頻繁に行うTCPではこれは、まるで別の世界になってしまいます。

* * *

開発:されそれで、RTT 0.5ms程度の隣のマシンとの転送ですが、sendのサイズは1500バイトで十分に1Gbpsを使い切ることがわかりました。

開発:一方UDPの成績は芳しくありません。どうも性能が出ない…

* * *

開発:えーと、調整をした結果、まずまずの性能に落ち着きました。

受信側
送信側

開発:ペイロードぶんのスピードとして TCPでは 940Mbps出ていますので、理論上の最高性能かなと思います。一方UDPでは 880Mbps程度。連続して送るとシステムのバッファが溢れてしまうので、4マイクロ間隔で1430Bを送っています。これだと、バッファがいっぱいだよエラーが出ませんし、受け側もほぼ100%受け取れます。これ以上間隔を短くすると、送り側としてはTCP並の速度で送れますが、受け側がボロボロになります。

開発:ああ、でも3usなら送信側が再試行すれば100%受け取られますね。

3us間隔送信
2us間隔送信
1us間隔送信

無間隔送信

基盤:ぼろぼろですねw

社長:要するにLANというかレイテンシーがサブミリ秒の近接したノード間でUDPを使うメリットは無いということですね。

開発:そう思います。で、問題はレイテンシーが大きい時はどうか、ということで、本題になります。なお、この実験は電力を約60W食うことが観測されています(笑)

* * *

開発:まあこれは実は最初にやって結果はわかっていまして、比較のためにマシン内や近接マシンで試すのに時間をとられてしまいました。

開発:ではまずは最果ての地、ドイツフランクフルト支所。RTT 250ms 超です。ここはscpのアップロードで1MB/sしか出ないことがわかっています。このベンチマークでもTCPでのデータ送信は同様な結果になります。

基盤:しかもスピードが時々刻々かわっている。ネットの状況に左右されてるんでしょうね。遠隔との通信の速度は、非常に不安定であることは実感としてわかっています。

開発:そしていよいよ、UDPではどうか。

基盤:50MB/s で届いていますね!50倍速

開発:まあ、半分落ちてますけどね。

社長:といいますか、我社から社外に向けて実際にギガビットでデータを送出したのはこれが初めてですね。

開発:それでこの数値は、相手がドイツでも東京でも大差無いんですよ。つまり、うちのインターネットは、上りは500Mbpsしか出ないのではないかという気もします。

社長:ライトセール以外の相手にためしてみたいところですが。CPU負荷的に厳しいとか。

開発:全般的には余裕ですね。30%食うことはないです。なにせデータを受けるだけで何も処理してませんし。

開発:たとえばライトセール東京にある我社ネームサーバ。

開発:もともとパケットがロスするのは織り込み済みですから、到達してないパケットのマップを返してもらって再送すればよいわけですが、ではどのくらいの速度までロスが起こらないかをためしてみす。… … 6.28MiB/s、50Mbps までは大丈夫っぽいですね。

開発:161マイクロ秒間隔での送信です。

社長:刻みましたねw

開発:普通のデータは1ビットでも落ちたらいけませんが、画像とかはとりあえず届いたものから表示するとか、ありますよね。動画だったらなんだか画質が落ちるとか。

社長:これはそもそも、差分だけ送るという方法に相性が良いですね。rsync とはちょっと違う方向性を考えると面白いかも。

基盤:ところで我社の$5ライトセールは2TB/月という制約ですが、どういうふうにリミットしているんでしょう?もうけっこう上限のような。

開発:ドイツ支部は暇だし死んでも大丈夫ですw でも、テスト用のインスタンスを作ってやってみますかね。

経理:まさか、上限を超えたぶんは従量課金になるとかではないですよね…

— 2020-0809 SatoxITS

そうだ、Shellを作ろう

開発:プログラマとして生まれたからにはやはり、一度は自分のshellを作ってみたいものだと思うわけです。

基盤:どういう生まれ方をしたんですか。

開発:私がCプログラマになったころにはまだ、Unix V6でしたから、V6のshellしかなかったんです。ヒストリを参照するのにいちいち !なんとかを入れないといけない。面倒なのでカーソルで昔のヒストリに戻れるようなのを作りましたよ。それとか、プログラムのPATH同様のデータのPATHを定義して、参照したデータファイルをそのPATHで探索して開くようなこともしました。ファイル名を入れるとそのディレクトリにジャンプするとか。

開発:最近はzshやsshに、大いに刺激されました。コマンドラインシェルは不滅だなという気もしました。ですが、コーディングを見るとまあ、昔ながらのCなわけです。それはそれで好きなわけですが、今はもうそういう時代では無いのではないかなという気もするわけです。

開発:shell の重要な役割は、実行するプログラムの外部環境を整えて提供することだと思うんです。環境としてはまず、ファイルディスクリプタで与えたり、動的ライブラリとして与えたりするもの。あるいはファイルシステムの可視性を与えるかも知れない。実行のタイミングを与えるかも知れない。

開発:Unixがくれた重要な概念として標準入出力というのがあるわけです。これはコマンドのパイプラインを筆頭として、プログラムを部品化して再利用性を高めるのに大いに有益でした。そのうえ、入出力をどこにつなぐかというコーディングを各プログラムからなくすことができた。まあ必ずしもそうはなっていませんが。

開発:例えば、shellが開いてファイルディスクリプタとしてプログラムに与えて上げられるのはふつう、ローカルファイルかパイプだけなわけです。ですが、今やもちろん、リソースもさまざまだし、つなぐのだってパイプとは限らない。

開発:あるいは、コマンド引数というものを各プログラムが解釈するからてんでばらばらになったりするわけですが、shell が標準的な引数フォーマットを解釈して前処理して、プログラムは解釈結果だけを利用するようにすれば、もっと統一性がとれるし、プログラムのコーディングも省けるはずです。

社長:で実装の方針は?

開発:今から作るならやはりGoベースだと思うんです。豊富なパッケージを組み込みコマンドとして使えるし。

基盤:そういうのって既にあるんじゃないですかね。

開発:いや、既にあるとか関係ないです。これは、自分なりのshellを作ってみたいという欲求に突き動かされているものです。そして、実際自分なりに作って改良して行くと、既存のものには無かった何かが、必ず生まれるものです。無知や誤解はオリジナリティの父になります。だからこの件については、既存のものがあるか調べ物はしません。

開発:第一次トライアルは3日程度のスプリントで考えたいと思います。

社長:何という名前にしますか。

開発:まあとりあえず安直に、gsh ですかね。

社長:承認。

* * *

開発:というわけで、第1版ができました。こんなかんじです。

開発:機能的には、golang の eval が出来るようにした点と、コマンドを実行するごとに実行時間を自動表示する点が特長です。実装上は、fork して exec する方法を調べるのに少し手間取りました。現在のコードはこんな感じ。

社長:内部コマンドも実行時間を表示してくれると良いですね。

基盤:システムコールナマ呼び出しですか…

開発:Goの皮をかぶってると自由が効かないんですよ。データ構造的には Goの機能を活用させてもらいますが、外部とのやりとりというか結線は、ナマでやろうと思います。

社長:良い感じですね。ちょっと食事に行ってきます。

* * *

社長:帰りました。またえびすの小瓶のラーメンやでした。

経理:大変な事になりました。これを見て下さい。

経理:左が一昨日の電気使用量。ほぼ200Wべたです。右が昨日。零時頃にエアコンをつけて8時に切り、ついでに各種の無駄機器の電源を切りました。

社長:劇的ビフォー・アフター。

基盤:これ、スケールが変わっちゃってますね。そろえるとこうなります。

開発:つまり一昨日まで200Wだったのが、昨日深夜から朝まで400Wになり、その後100Wになった。

基盤:ただ不思議なのは、UPSの電力計は常時150Wを超えていますから、100W刻みのてぷこでんこ計では200と100が交互になりそうに思うんですが、このグラフではほぼ100Wで推移していることです。

社長:まあ、これが逆だったら問題ですけどね。しかしうちって100Wで維持できちゃう会社なんだなあ。

経理:こうなるともう、基本料金が気になってきますね。契約の容量を下げましょうか?

社長:まあ話としては面白いですが… いざという時のこともあるし。他の方面に残っている巨悪から片付けたほうが良さそうです。

開発:あれ?でんこ計の縦軸ってkWhですね。一つの棒は30分毎。つまり30分に0.1kWhてことで、1時間平均にすると0.2kWh、つまり平均消費電力200Wということなのでは?

社長:あ。なるほど… それなら計算が合います。

基盤:なんにしても一昨日まで平均400W使ってたのを、昨日から200Wに削減できたと。照明を落としたり、無駄に電源に電源ONのまま待機していたテレビやオーディオを切った効果でしょうか。何ヶ月、というか何年も無駄に200W食い続けてたなんて、恐ろし過ぎる…

基盤:ああ、それはそうと、このPoerMeter、結局 macOS版はしょっちゅ接続不能になってしまうので、Lenovoで動かすことにしました。いまのところ朝からずっと快調です。

* * *

開発:ふぁ。あ。ぁあ。あー、よく寝た。

社長:よく規則正しい生活をって言いますが、寝たい時に寝て食べたい時に食べる。どっちが身体にいいんでしょうね?

基盤:まあ無理に寝る必要が無いので、不眠症というものはこの生活スタイルには存在しないですね。

* * *

開発:第2版ができました。こんな感じです。

開発:今回は repeat というコマンドを入れました(4>)。nop というコマンドを1,000,000 回実行して所要8.6ms、つまり一回あたり8.6ns であることがわかります。

社長:さすが3GHzのCPUですね。私がこないだまで触ってた100MHzのARMとはえらい違いです(^-^)

開発:それと、lets.go というファイルを “go run lets.go” なしで、単に lets で実行できるようにしました(6>)。

開発:あと、外部コマンドの入力ファイルや出力ファイルを -i ifile -o ofile command のように指定できるようにしました。まあ通常のshellなら、command < ifile > ofile なところです。

開発:全般的に、とにかくシンプルに左から実行して行くという型式です。

社長:まあ中置記法は実は人間にも優しくないという事もあるかも知れません。

開発:実行の順序を変えるには、その先に実行するコマンドをプッシュできると良いのかなと思います。

社長:コマンドや実行結果をスタックに積んでポーランド記法的に結果の掛け合わせができると面白いでしょうね。ある意味 repeat は、積んだり降ろしたりの例ですね。

開発:ということで、現在のコードはこんな感じです。

社長:今300行ぐらいですね。どのくらいで落ち着きそうですか?

開発:まあ1000行あれば、そこそこshellっぽいものになるかなと。3000行くらいで、けっこう実用的なものになる気がします。Goのパッケージに部品が豊富にあるので、いかに活用できるかかなと思います。

* * *

ブラウザの色好み

社長:ただいま帰りました。

経理:酒臭いですね。

社長:今日は12LSUを投じて我社枢軸の精神衛生および創造性に多大の寄与をもたらす、… というか、部屋がやけに暗いというか。

経理:照明を絞ってみました。測るのが難しいのですが、数ワット削れている可能性があります。

開発:蛍の光窓の雪のような。

基盤:ホタルの発光は有機ELの元祖なんでしょうけど、普通窓の雪は発光しないですよね。ヤバいものが混じってなければ。

社長:君はチェレンコフの光を見たことがあるか。

開発:融解の熱が何らかの作用で光に転じているとか。

社長:常温核融合もびっくりですが。新しい触媒?まあ生体なら有り得るんでしょうね。雪の結晶の形をした微生物だったとか。

基盤:スクリーンセーバを合成で作るなら、ホタルが飛び交ってるような風景を作って欲しいですね。

開発:線香花火とか。

社長:おばけ屋敷とか。

経理:三流紙に書かれそうですね。社内はまるでお化け屋敷のようだった(捜査員談)

広報:メディアを呼んで何かプレス発表してみたいです。

社長:うーむ、ブラックライトは清涼感を醸すのに良いかも知れないですね。

社長:ああそれで飲んでる時に、前々から気になっているFirefoxの色使いについて、考えたのですが。

社長:あれっていわゆるRGBモデルじゃなくて、アドビモデルとかいうやつなんじゃないだろうかと。

基盤:PDF臭いですね。セピアっぽい。

開発:モニタで見るようには出来てない?

社長:主要5+1ブラウザを並べてみます。

社長:まあ、ひと目見て右下がFirefoxだとわかるわけです。色で。

基盤:うーん、その他も、どれが何のブラウザかわかるようになりましたw

開発:というか、ブラウザごとにアイコンはあるんで、それを右下あたりに主張してもいいんじゃないですかね。ユーザの設定でもいいですけど。

社長:画像の圧縮のアルゴリズムというかポリシーはCSSとかでも既定されていると思うんですが、たぶん細部は実装依存。で、Firefox以外はOSが提供しているライブラリか、Chromiumの共通ライブラリを使ってるんじゃないだろうかと思うわけです。

開発:なんにしても印刷用のヘッダのバナーは、ブラウザの生成に任せるのではなくて、明示的に提供してあげたほうがよいでしょうね。著作者もびっくりな色で印刷されたら、権利問題じゃないかと。

社長:PDF といっても、画面で見る時と印刷する場合がありますから、用途を選択できると良いと思います。

基盤:自動変換できるんじゃないでしょうか?

社長:それはそうと、今日は帰社して快適だなと思ったら、私の体調のせいではなくて室温が29度を切っていますね。

経理:ずっと窓を締め切ってエアコンをつけっぱなしにしていると気が付かないですよね。

開発:今日は洗濯日和とか気象予報士が言ってますけど、今日はこういう風向きだから、窓を開けて自然の風を楽しむ日和ですみたいな事を言ってほしいですね。言ってるのかも知れないけど。

社長:花粉予報はみっちりやってますよね。

基盤:まあ民放とかでは公共の電波でくだらない星占いとか流してますけどねw

社長:私もいい年まで血液型性格判断を受け入れてた口だし(笑)。民放はエンタメなんだからいいんじゃないですか。スポーツの中継しか見ないけど。

— 2020-0807 SatoxITS