expect for Command Prompt by GopherLua

コマンドプロンプト向けの expect を Go 言語で作った。

特徴

  • スクリプトLua で書く。GopherLua を使ったので、lua53.dll は不要
  • 画面のプロンプトを待つ except() 関数は、本家だと標準出力・標準エラーを監視するが、こちらは「現在カーソルがある行とその上の行」を0.1秒間隔で監視させている
    • git付属のOpenSsh のパスワード入力の際のプロンプトの出力先が標準出力・標準エラー出力のどちらでもないため、本方式を採用した。
    • kernel32.dll の ReadConsoleOutput という API を使ったが、これ、バッファの確保の仕方のルールが分からず、えらく苦労した。
  • コマンドに入力内容を送信する send() 関数は、本家だと標準入力に文字列を流し込むが、こちらはキーボードが叩かれたのコンソールイベントを発生させている

使用例

実装した関数は spawnexpectsendだけなんだけど、Lua なのでいろいろ柔軟なことが出来る。

if spawn([[c:\Program Files\Git\usr\bin\ssh.exe]],"foo@example.com") then
    expect("password:")
    send("PASSWORD\r")
    expect("~]$")
    send("exit\r")
end

作った感想

  • UNIX/Linux サーバ相手に本気で expect とかやりたい人は、TeraTerm マクロを使う
  • Windows での動作の自動化は Command Prompt だけではどうにもならない場合が多い。

使ってもらえるシーンないな!(まだ技術の無駄遣い)

git for Windows 付属の ssh でのログインを自動化したい

Linux なら expect というツールがあるけど、Windows の場合、どうしたらいいんだろう」などと思って、API を調べていたら、WriteConsoleInput などというコンソールイベントを「捏造」できる API があるのを発見。

ちょっと、ツールを作ってみた。

package main

import (
    "os"
    "syscall"
    "unsafe"
)

var kernel32 = syscall.NewLazyDLL("kernel32")
var writeConsoleInput = kernel32.NewProc("WriteConsoleInputW")

type inputRecordT struct {
    eventType         uint16
    _                 uint16
    bKeyDown          int32
    wRepeartCount     uint16
    wVirtualKeyCode   uint16
    wVirtualScanCode  uint16
    unicodeChar       uint16
    dwControlKeyState uint32
}

type Handle syscall.Handle

func NewHandle() (Handle, error) {
    handle, err := syscall.Open("CONIN$", syscall.O_RDWR, 0)
    return Handle(handle), err
}

const (
    KEY_EVENT = 1
)

func (handle Handle) WriteRune(c rune) uint32 {
    records := []inputRecordT{
        inputRecordT{
            eventType:         KEY_EVENT,
            bKeyDown:          1,
            unicodeChar:       uint16(c),
            dwControlKeyState: 0,
        },
        inputRecordT{
            eventType:         KEY_EVENT,
            bKeyDown:          0,
            unicodeChar:       uint16(c),
            dwControlKeyState: 0,
        },
    }
    var count uint32
    writeConsoleInput.Call(uintptr(handle), uintptr(unsafe.Pointer(&records[0])), 2, uintptr(unsafe.Pointer(&count)))
    return count
}

func (handle Handle) WriteString(s string) {
    for _, c := range s {
        handle.WriteRune(c)
    }
}

func main() {
    console, err := NewHandle()
    if err != nil {
        println(err.Error())
        return
    }
    for _, s := range os.Args[1:] {
        console.WriteString(s)
        console.WriteRune('\r')
    }
}

これをビルドして typekeyas.exe というコマンドを作って「typekeyas "dir /w"」などと打つと、あたかも直後にコマンドプロンプトに対して自分が「dir /w」と打ったかのようにディレクトリが表示される(いや、引数の中で打ってはいるんだけど)

これを使ってパスワードを打たせてみよう(以下、一部伏字)

$ typekeyas (PASSWORD) & "c:\Program Files\Git\usr\bin\ssh.exe" XXXXX@XXXXX.org
XXXXX@XXXXX.org's password:
Last login: Wed Jun  7 22:09:15 2017 from XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
FreeBSD 9.1-RELEASE-p24 (SAKURA17) #0: Thu Feb  5 10:03:29 JST 2015

Welcome to FreeBSD!

nyagos の場合はコマンド区切り文字の「&」を「;」にすればよいが。。。 これパスワードがヒストリに残ってしまうからすごく危険なんだな!

で、「これ画面の出力も拾えれば、完全な expect が出来るのでは?」とか思ったが、最初の XXXXX@XXXXX.org's passwordという行だけに限って標準出力・標準エラー出力ともに出ていない(他は出てる)。コンソール(CONOUT$)に直接出しているんでしょうかね…

Thunderbird で External Editor(外部エディター起動アドオン) が動かなくなった俺達は

エディターが起動していないのに、エディターの終了待ちでハングしてしまう。 どうやら Thunderbird が52へアップデートしたタイミングで、動かなくなったアドオンがいろいろ出ているようだ。 (参考:Thunderbird 52.0 Release - とりかごとなり。

ググってみたところ、External Editor のサイトの掲示板( http://globs.org/articles.php?lng=en&pg=2 )に下記の書き込みがあった。

どうやら、原作者ではなく、ユーザで修正版を作った方がいらっしゃるようだ。ありがたや。

インストールされている External Editor を削除して、リンク先の exteditor_tb52_v101.xpi を アドオン画面にドラッグ&ドロップしたところ、 External Editor が前のように起動されるようになった。よしよし

テスト用のWindows10の仮想環境(寿命90日)の作成と日本語化

(1)90日限定の試用版 Windows10 をダウンロード

OVAイメージ(4GB)をダウンロードできる。 このイメージは一度立ち上がると、90日後に使えなくなるが、立ち上げる前の状態のスナップショットに戻せば、何回でも使える(らしい:未検証)。 ただ、WindowsUpdate の手間暇を考えると、都度、ダウンロードしなおした方がよいかもしれない。

(2)VirtualBox をダウンロード&インストー

わたしの環境では VirtualBox が「Windows 7 または Windows Vista でプログラムをインストールしようとすると、"Windows インストーラー サービスにアクセスできませんでした」というエラーが発生して、インストールが止まってしまった。 この場合の対処法は Microsoft のサポートサイトに記載されている。

(3)VirtualBox の起動&イメージをスナップショット

  1. VirtualBox を起動 → ファイル(F) → 仮想アプライアンスのインポートより、OVFイメージをインポート
  2. 「MS Edge - Win10_preview」というマシンが出来るので、スナップショット(S)→ 📷 カメラアイコン(Ctrl-Shift-S)で、スナップショットを取る

(4)イメージの起動と WindowsUpdate

  1. VirtualBox をインストールした後、一度もホストOSをリブートしていない場合は、一度リブートする(これをしないとゲスト起動が失敗する)
  2. ゲストの「MS Edge - Win10_preview」を起動する
  3. ユーザは自動ログインされる。壁紙に全情報が書いてある
    • ユーザ名「IEUser」
    • パスワードは「Passw0rd!」
      • 最初のPは大文字、途中の0は数字のゼロ、末尾には感嘆符がついている
  4. とりあえず、WindowsUpdate しよう
    • ネットワークがオフラインになっているというエラーがでることがある。 実際にネットワークの設定が出来ていない場合もあれば、起動直後だからまだネットワークのサービスが起動しきっていないだけのこともある。 一度くらいためしに「リトライ」してみよう。
  5. 待つ(すごい時間かかる)

<このあたりでスナップショットを取っておいた方がよいと思われる>

(5)日本語化

  1. 「Setting」
    • →「Time and language」
    • →「Date & Time」
    • → Time Zone を「(UTC+9:00) Osaka, Sapporo Tokyo」にする
  2. 「Region & language」
    • Countory & region」を「Japan」
    • 「Language」で「Add a language」「日本語」
    • 「日本語」をクリック
      • 「Set Default」
      • 「Language Options」→「Download」
      • 「Hardware keyboard layout」→「Change layout」→「Japanese keyboard(106/109 keys)」& Sign out

(6)コマンドプロンプトのchcp 932 対応

  • Windowsアイコン」を右クリック
  • →「コントロールパネル(P)」
  • →「時計・言語・および地域」
  • →「日付、時刻、または数値の形式の変更」
  • →「管理」タブ
  • →「Unicode対応ではないプログラムの言語」
  • →「システムのロケールの変更」
  • →「日本語(日本)」
  • →「今すぐ再起動」

<ここまでが結構な手間なのでスナップショットを取っておいた方がよいと思われる>

以上

import を全て絶対パスにした

こんな感じに

-       "../completion"
-       "../shell"
+       "github.com/zetamatta/nyagos/completion"
+       "github.com/zetamatta/nyagos/shell"

これにより:

  • ソースを GOPATH 内に置けなかった
  • go get github.com/zetamatta/nyagos で全ソースをダウンロードできなかった。
    git clone http://github.com/zetamatta/nyagos/ は可能)

が解消する。そのかわり

  • ソースの置き場が $GOPATH/src/github.com/zetamatta/nyagos と一意になってしまう。
    • amd64 用のビルド場所はこちら、386 用のビルド場所はこちらと、複数設けることができなくなってしまう。
      (lua53.dll が 64bit 用も 32bit 用も同じ名前なので、同じ場所におけない)

という問題が発生してしまう。

いろいろと検討した結果、とりあえず現行の自分のビルド環境は次のようにすることにした。

  • メインのメンテナンスディレクトリは ~/go/nyagos から ~/go/src/github.com/zetamatta/nyagos に変更 (ここでamd64用ビルドする)
    • 移動が大変なので lnk ~/go/src/github.com/zetamatta/nyagos ~ でショートカットを張る
  • 386 ビルド用ディレクトリは従来どおり ~/go/386-nyagos のまま
    • ただしビルド専用。import の先が ~/go/src/github.com/zetamatta/nyagos になってしまうので、ソースの編集はこちらではできない

でいこうと思う。

将来的には ~/go/src/github.com/zetamatta/nyagos のいたまま amd64 も 386 もビルドできるようにしたい。 そのためには、今 ~/go/src/github.com/zetamatta/nyagos 直下にある lua53.dll を適当なサブフォルダーに移動させなくてはいけない。 でも、lua53.dll って git に登録していないバイナリファイルなので、移動させるとなるとソースからビルドしてくれているユーザにそれをアナウンスしなくちゃ いけないんですよね…むーん。

lua53.dll も git に登録しちうという手もあるが、そうすると git clone とか go get でバイナリファイルがダウンロードされるので、 アンチウィルスにひっかかってしまう可能性が出てきてしまうという…

将来的に $GOPATH 以下でも、相対パスの import ができるようにならないですかね (これ、なぜできないようにしたんでしょうね)

追記

「386 と amd64 で $GOPATH を分ける」という方法を思いついた。いずれやってみよう

まだ、NYAGOS 4.2 は出せない

Luaインスタンスのクローンについては今のところ問題は出ていない。 無論、不具合の一つ二つは出たが、 確認当日中にあっさりと解決できて、この程度で済むのなら上出来だったと思う。

また、もう一つの懸案も解決できた。

これは Args[] を Windows 風の一枚引数文字列に展開する際の二重引用符の扱い方のために、 FIND コマンドが期待するような「検索文字列の前後には二重引用符をつける」という仕様を呼び出し時に満たすことができないという問題だ。

この issue 自体は最近起案したものだけれども、Args[] の展開を自前でやるというのは前々からの課題だった。 やろうと思うと、API の CreateProcessW を呼ぶライブラリから作られなばならないと思っていたので、 すぐには実現できないと思っていた。 が、Go のライブラリのソースを見てたら、実は Args[] の展開を自分でやる手段が提供されていることが分かり、 思わぬタイミングで懸案は解決した。 詳細は前に書いた記事を参照のこと。

というわけで、結構大きい課題が二つの解決して、満を持して 4.2.0 をリリースしたいところだが、もう一つ欲張りたいところがある。

それは foreach とか if ~ then ~ endif とかの制御構文。 nyaos-3000 は、その辺をちゃんと実装していたが、nyagos は Lua で全部やればいいからと完全にスルーしてしまった。 ところがユーザの傾向を見ると、Lua だからカスタマイズの仕方が分からない、調べれば分かるが、学習コストに見合わないという人が多い。 完全に計算違いだった。

foreach を実装するには、コマンド列の任意の位置へ移動できなくてはいけない。 そのためには shell パッケージは単品のコマンドの実行だけではなく、前後のコマンドも含めたセッションも管理して、 コマンド列の移動可能箇所をマーク&移動できるようにしなければいけない。 これまでは一連のコマンド列の管理は mains パッケージ(旧mainパッケージ)で行っていた。 昨日・今日で、それをようやく shell パッケージ(旧interpreterパッケージ)側に移動した。 結果、mains パッケージからは「shell.Loop(コマンド列読み込みオブジェクト)」だけを呼ぶだけとなった。

これ、日本語で書くと簡単に見えるが

  • shell パッケージは Lua へ依存させてはいけない。Lua への依存は mains パッケージが隠蔽するか、注入するかしてやらないといけない
  • インタラクティブシェルか、スクリプトかの管理に shell パッケージは関与してはならない

とか制約が多くてたいへんだった。が、最終的にはコマンド列読み込みインターフェイス

package shell

type Stream interface {
    ReadLine(context.Context) (context.Context, string, error)
    GetPos() int
    SetPos(int) error
}

をベースにうまいこと実装できた。おかげで shell パッケージは奇麗さを維持できた。 が、mains パッケージはかなりグダグダである。 「依存性」が全部 mains パッケージに集まってるのだから仕方がない。

Go for Windows で子プロセスに二重引用符を引数でそのまま渡したい時

問題の症状

親プロセスのソース:

// exec1.go
package main

import (
    "os"
    "os/exec"
)

func main() {
    c := exec.Command("foo", `"<BAR>"`)
    c.Stdout = os.Stdout
    c.Stderr = os.Stderr
    c.Stdin = os.Stdin
    c.Run()
}

子プロセスのソース(バッチファイル):

@rem foo.cmd
@echo %0 %*
@exit /b 0

実行すると

$ go run exec1.go
foo \"<BAR>\"

二重引用符の前にバックスラッシュがついてしまう。

解決方法

バックスラッシュが付かないようにするには、Go言語標準のexec.Cmd.Args[]Windows 形式の引数形式に展開する処理をパスして、自前で直接指定すればよい。

//exec2.go
package main

import (
    "os"
    "os/exec"
    "syscall"
)

func main() {
    c := exec.Command("foo")
    c.SysProcAttr = &syscall.SysProcAttr{CmdLine: `foo "<BAR>"`}
    c.Stdout = os.Stdout
    c.Stderr = os.Stderr
    c.Stdin = os.Stdin
    c.Run()
}
$ go run exec2.go
foo "<BAR>"

なぜ、こんなことをする必要があるのか

FIND.EXE や CMD.EXE は、引数につけられた二重引用符の有無で、その引数がどういうものか判別しています。このため、Goのプログラムから FIND.EXE が意図どおりに呼べないという問題が発生していました。

こういう仕様って、どこに書いてあったの?

ここ