Tunnelblick を JXA で操作する

SSL-VPN クライアント Tunnelblick は AppleScript で操作可能です。

私は AppleScript がよく分からないため、JXA の出番です。とはいえ、スクリプトエディタのライブラリ（ヘルプ）は読みづらい。AppleScript のヘルプを JavaScript 用に自動変換しているからとはいえ、悩ましいですね。とはいえ JXA も通常の JavaScript とは違う独特さがあり、JXA でも試行錯誤がつきものです。

JXA でできる Tunnelblick の操作方法をメモします。

試した環境：

- *Tunnelblick: OS X 10.13.6; Tunnelblick 3.7.6a (build 5080); prior version 3.7.6 (build 5060)

# アプリケーションオブジェクト

JXA お約束の `Application(アプリ名)` でアプリケーションオブジェクトを取得します。

```js
let tb = Application("Tunnelblick");
```

インスタンス名はなんでもいいですが、この記事では `tb` にします。

# 設定確認

`tb.configurations` です。

`tb.configuraitons` は `()` を伴ってメソッド呼び出しをすると配列を返します。

```js
let configurations = tb.configurations();
console.log(configurations.length); // 設定の数
```

`osascript -i -l JavaScript` インタラクティブ実行で試してみるとこんな感じ。

```
>> conf = tb.configurations()
=> [Application("Tunnelblick").configurations.byName("xxx"), Application("Tunnelblick").configurations.byName("yyy"), Application("Tunnelblick").configurations.byName("zzz")]
>> conf.length
=> 3
```

`xxx` といった設定名は仮のもの。この設定名は、Tunnelblick に入れた設定の設定名そのものです。

しかし、**この配列の各要素を取り出して、設定オブジェクトのメソッドを呼び出したりと行ったことはできません**でした。 上記出力にあるような `byName()` も使えず。 `tb.configurations()` 呼び出しは、設定の数を `tb.configurations().length` として把握するために使用する以外の活用法が無いような…。

そのかわり、 `tb.configurations[i]` といった `[i]` で添字を伴った配列のような呼び出しをすることで、各オブジェクトが取れます。実際に `tb.configurations()[0].name()` はエラーになりますが、 `tb.configurations[0].name()` は設定名を返すことで確認ができると思います。なお `tb.configurations[i]` のように配列っぽくアクセスできますが、 `tb.configurations.length` といったプロパティへのアクセスはできません。つまり純粋な配列 (Array) ではないということです。

```js
console.log("設定一覧");
let tb = Application("Tunnelblick");
let confs = tb.configurations;
for (let i = 0; i < confs().length; i++) { // confs.length だとエラー
    let c = confs[i]; // Configuration オブジェクト
    let name = c.name();
    let state = c.state();
    console.log(name + " : " + state);
}
```

`tb.configurations` の返り値を入れた `confs` 変数の扱いが独特ですね。JXA、こういうところが普通の JavaScript っぽくなくて、悩むことが多いです。

マニュアルを読むと、上記のようにして取り出した Configuration オブジェクトには以下のゲッターメソッドがあるようです。なお、**全てリードオンリー (r/o)** です。

| メソッド名 | 概要 |
|---|---|
| name  | 設定名を返却する。 |
| state | 接続状態を返却する。 |
| autoconnect | 自動接続をするかを返却する。 |
| bytesin | クライアントの受信バイト数を返却する。 |
| bytesout | クライアントの送信バイト数を返却する。 |

state の値について、ドキュメントに載っているものは EXITING と CONNECTED のみですが、接続途中にはいくつかの表示があるようです。見つけたら追記します。

| 値 | 意味 |
|---|---|
| EXITING | 切断状態 |
| CONNECTED | 接続状態 |
| WAIT | 待機中 |
| AUTH | 認証中 |
| GET_CONFIG | 設定取得中 |
| CONNECTING | 接続中 |
| RECONNECTING | 再接続中 |
| SLEEP | . |

流れは EXITING → CONNECTING → AUTH → GET_CONFIG → CONNECTED だと思いますが、確実な情報ではありません。

autoconnect の値については以下。

| 値 | 意味 |
|---|---|
| LAUNCH | Tunnelblick 起動時に接続する |
| START | OS起動時に接続する |
| NO | 自動接続は行わない |

# 接続と切断

接続と設定はアプリケーションオブジェクトのメソッドから行います。なお、設定オブジェクトには接続と切断のメソッドはありません。

以下のように、特定のメソッドの第一引数に設定名の文字列を与えるようにして使います。

```js
let tb = Application("Tunnelblick");
tb.disconnect("xxx"); // xxx は設定名
tb.connect("yyy"); // yyy は設定名
```

接続と切断に関するメソッドは以下。

| メソッド名 | 概要 |
|---|---|
| connect(CONFIG_NAME) | 指定した設定名で接続を開始 |
| disconnect(CONFIG_NAME) | 指定した設定名の接続を終了  |
| connectAll() | 全ての接続を開始 |
| disconnectAll() | すべての接続を終了 |
| disconnectAllExceptWhenComputerStarts() | OS起動時に接続する設定以外の全ての接続を終了 |

マニュアルに載っているメソッドは上記が全てです。設定自体を作成したりといったコマンドはありませんが、通常 Tunnelblick を使用する上で欲しい機能は接続と切断の管理がほとんどだと思うので、だいたいの方が満足するかなと思います。

# アプリケーションサンプル

JXA 全般の話題ですが、 `#!/usr/bin/osascript -l JavaScript` というシェバン行を書いておくと、当該スクリプトに実行環境を付与することで、パスの通ったところに置いてコマンドとして使用することができます。

引数を取る場合には `run` 関数が必要となります。Bash シェルスクリプトとして作成して、引数管理を行った後でシェルスクリプト内で動的生成した JXA スクリプトを実行する方法（私が場当たりスクリプトで好んでやる手法です）もありますが、以下では引数を取る場合には `run` 関数による手法を採用しています。

## 設定と接続状態を一覧

設定を取得することができるので、一覧できるコマンドを書いてみます。

```js:tbconflist.js
#!/usr/bin/osascript -l JavaScript
'use strict';

let tb    = Application("Tunnelblick");
let confs = tb.configurations;
for (let i = 0, max_i = confs().length ; i < max_i ; i++) {
    let conf = confs[i];
    let name = conf.name();
    let state = conf.state();
    let autoconnect = conf.autoconnect();
    let [b_in, b_out] = [conf.bytesin(), conf.bytesout()];
    console.log(`${name}: ${state} (autoconnect: ${autoconnect}) [in: ${b_in} out: ${b_out}]`);
}
```

## 接続と切断の状態をトグルする

第1引数に設定名を取り、それが接続状態のときは切断、切断状態のときは接続をするスクリプトです。

```js:tbtoggle.js
#!/usr/bin/osascript -l JavaScript
'use strict';

const DEBUG = false;
// 別に console オブジェクトにぶら下げなくても良いけれど
console.debug = function(string) {
    if ( DEBUG ) console.log("DEBUG: " + string);
};

function run(argv) {
    let config_name = argv[0];
    console.debug(`name is ${config_name}`);
    if ( !config_name ) {
        throw "give config name as 1st argument";
    }
    let tb = Application("Tunnelblick");
    let config = get_config_array(tb).find( (c) => { return c.name() === config_name; } );
    if ( !config ) {
        throw "config not found";
    }
    toggle_tb_connect_state(tb, config);
}

function get_config_array(tb) {
    let confs = tb.configurations;
    let config_array = [];
    for(let i = 0; i < confs().length; i++ ) {
        config_array.push(confs[i]);
    }
    return config_array;
}

function toggle_tb_connect_state(tb, config) {
    let config_state = config.state();
    let config_name  = config.name();
    console.debug(`state is ${config_state}`);
    if ( config_state === "EXITING" ) {
        console.debug(`connect(${config_name}) because config_state is ${config_state}`);
        tb.connect(config_name);
    } else if ( config_state === "CONNECTED" ) {
        console.debug(`disconnect(${config_name}) because config_state is ${config_state}`);
        tb.disconnect(config_name);
    } else {
        console.log(`config ${config_name} state is ${config_state}. exit.`);
    }
}
```