tmytのらくがき

個人の日記レベルです

ブラウザでTerminal実装してみたら簡単だった

新年あけましておめでとうございます。本年もどうぞよろしくお願いいたします。

というわけで、Webブラウザで動くターミナルを実装してみたんです。NodeJSで。そしたらすごく簡単だった。って話です。

使うもの

  • express
  • pty.js
  • socket.io
  • xterm.js
  • pug-static

インストール

npmでてきとうにインストールします。

npm init
npm install --save express pty.js pug-static socket.io xterm

サーバ実装

適当にかきます。xterm.jsの中身をexpress.staticで公開しつつ、viewはpugで書きます。

ブラウザとの通信はSocket.IOを使って、ブラウザとターミナルの間をそれぞれ中継してあげます。

'use strict';

const express = require('express')
    , app = express()
    , server = require('http').createServer(app)
    , pugStatic = require('pug-static')
    , Io = require('socket.io')
    , pty = require('pty.js')

app.use('/xterm.js', express.static('node_modules/xterm'))
app.use('/', pugStatic('views'))

let io = new Io(server);
io.on('connect', socket => {
  let term = pty.spawn('bash', [], {
    name: 'xterm-256color',
    cols: 80,
    rows: 24
  });
  term.on('data', d => socket.emit('data', d));
  socket.on('data', d => term.write(d));
  socket.on('disconnect', () => term.destroy());
});

server.listen(3000);

クライアント実装

こっちもてきとうに。これは、index.pugという名前でviewsディレクトリの中に保存して使います。

サーバ実装と同様に、Socket.IOでブラウザへの入出力を中継してあげます。

doctype html
html
  head
    link(rel='stylesheet', href='/xterm.js/dist/xterm.css')
  body
    #terminal
    script(src='/xterm.js/dist/xterm.js')
    script(src='/socket.io/socket.io.js')
    script.
      var term = new Terminal();
      var socket = io();
      term.open(document.getElementById('terminal'));
      term.on('data', d => socket.emit('data', d));
      socket.on('data', d => term.write(d));

おわり

あとはアクセスすると、bashが見れて、そのまま使えます。xterm.jsすごくって、vimとかtmuxとかちゃんと使えます。

ただ、この実装にユーザ認証が含まれていないので必ずユーザ認証して使ってください。

Surface Dialを2個接続してSOUND VOLTEXごっこした人へ

Surface Dialを2個以上接続していたとしても、正規のAPIからアクセスするといくつあっても1個にしか見えないのは周知の事実です。じゃぁどうにかして、2個認識できないのかなぁ…ということでやってみました。

2019/3/31: 今見返してみたら、3バイト目から2バイトが符号付で回転角やん…本文は修正済みです。

TL;DR

  • 通常のAPIからは1個しかみえないが、HIDデバイスなので直接読めば読める
  • VID:045E, PID: 091B, UsagePage: 0001, Usage: 000Eを読めば生データ見える
  • 生データの2バイト目の下位1bitが押し下げフラグ、3バイト目から2バイトが回転量(signed)

バイス構成

f:id:tmyt:20161127132805p:plain

これを見ると、HID over GATTで5種類のデバイスが見えていて、UsagePage, Usageは次の通り

UsagePage Usage
0001 0080
0001 000E
0001 0072
FF07 0070
FF07 0071

一番上はWinRTでブロックされているUsageなので今回は省略。ほかの4つは特にWinRTでブロックされてない(!!)のでアクセスしてみたところ、返事が返ってきたのは2個目のやつでした。

とりあえずアクセス

public MainPage()
{
    this.InitializeComponent();

    Loaded += async (sender, args) =>
    {
        var d2 = await GetAsync(0x0001, 0x000E);
        d2.InputReportReceived += D2_InputReportReceived;
    };
}

private void D2_InputReportReceived(HidDevice sender, HidInputReportReceivedEventArgs args)
{
    var dump = string.Join(" ", args.Report.Data.ToArray().Select(b => $"{b:X2}"));
    Debug.WriteLine($"D2: {dump}");
}

async Task<HidDevice> GetAsync(ushort up, ushort uid)
{
    var str = HidDevice.GetDeviceSelector(up, uid);//, 0x045E, 0x091B);
    var devices = (await DeviceInformation.FindAllAsync(str)).ToArray();
    return await HidDevice.FromIdAsync(devices[0].Id, FileAccessMode.Read);
}

こんなの書いて、ダイアルを回したり押したりするとこんなダンプがデバッグ出力で得られます。

D2: 01 02 02 00 0A 0B 0C 0D 3A
D2: 01 02 03 00 0A 0B 0C 0D 3A
D2: 01 02 02 00 0A 0B 0C 0D 3A
D2: 01 02 FF FF 0A 0B 0C 0D 3A
D2: 01 02 FF FF 0A 0B 0C 0D 3A
D2: 01 02 FE FF 0A 0B 0C 0D 3A
D2: 01 02 FF FF 0A 0B 0C 0D 3A
D2: 01 02 FD FF 0A 0B 0C 0D 3A
D2: 01 02 FD FF 0A 0B 0C 0D 3A
D2: 01 02 FD FF 0A 0B 0C 0D 3A
D2: 01 02 FC FF 0A 0B 0C 0D 3A
D2: 01 02 FB FF 0A 0B 0C 0D 3A

解析結果

このバイナリの中身をよく見てるとだいたいこんな構造みたい。

struct Report{
  byte One;
  byte Flags;
  short Degree;
  byte[5] Nazo;
}

2バイト目のFlagsとして名前を付けてみたところは、ビットフィールドになっているようでそれぞれ次の意味っぽい。

ビット 意味
1ビット目 押し下げ状態。1で押してる状態
2ビット目 回転中?

3バイト目から2バイトは符号付の値のようで、右回転で正の値、左回転で負の値になってる感じ。WinRTのデフォルトより元気で1度ごとにレポートがきます。

で、のこった5バイトはよくわからん。

まとめ

SOUND VOLTEXごっこできそうですね!

Surface Dialを2個接続するとどーなるの?

Q. Surface Dialって2個接続するとどーなるの?

BLEなHIDデバイスなので普通に接続できるはずだけどいったいどうなるの…?

A. APIからは1個に見える

var controller = RadialController.CreateForCurrentView();
controller.RotationChanged += (_, e) => {
  Debug.WriteLine(e.RotationDeltaInDegrees );
};

こうした時に、1つ目を回しても、2つ目を回してもそれぞれの回転量に応じた値がここでよばれる。 残念ながら別々のダイアルとして扱うことはできないみたい。

ちなみに、デバイスマネージャーからはこうみえてます。 f:id:tmyt:20161127132805p:plain

なのでいちおうAPIより下では別々のものにみえてました。

C++/Win32からSurface Dialをさわってみたよ

TL;DR

  • C++/Win32からRadialControllerなんて触るもんじゃない
  • ABI::Windows::Foundation::Collectionsに対する実装はSDKに含まれない
  • MIDLコンパイラ/ns_prefixが必須

AzureaでSurface Dialを扱えるプラグインを書きました

Surface Studioと同時に発表されたSurface Dial、回せて、クリックできるだけのデバイスなのです。 これがMicrosoft Global Summitへ参加したタイミングでちょうど発売されて運よく入手することができたので、 UWP*1も、WPF*2も、やってる人がすでにいるので面白そうなのでC++から触ってみました。

ただただ触ってみるだけじゃ面白くないので、せっかくだから2010年にWindows Mobile 5/6向けにリリースしたTwitterクライアントでSurface Dialが使えるようになるプラグインを書きました。

Surface Dialを制御するCOMオブジェクトを実装して、さらにそれをJScriptから制御してるって感じです。

gist.github.com

デスクトップアプリからSurface Dialを操作する

Surface DialをはじめとするRadialControllerははWinRT経由でOSに統合されているので、UWPからならとても簡単に扱えます。たとえばこんな感じに。

var controller = RadialController.CreateForCurrentView();
controller.RotationChanged += (sender, e) => { }
controller.ButtonClicked += (sender, e) => { }

このAPIですが、シグネチャを見てわかるように、UWPのViewを必要としています。 しかし、デスクトップアプリからだとViewが存在しないのでウィンドウハンドルを使ってインスタンスを生成できるなにがしが必要になってきます。 といっても、.NETからならそれほど大変なものでもなく、COMオブジェクトを経由してインスタンスを生成することができます。*3

[System.Runtime.InteropServices.Guid("1B0535C9-57AD-45C1-9D79-AD5C34360513")]
[System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIInspectable)]
interface IRadialControllerInterop
{
    RadialController CreateForWindow(
    IntPtr hwnd,
    [System.Runtime.InteropServices.In]ref Guid riid);
}

これでインスタンスを取得すればだいたいどうにかなります。ですが、なかなかそれなりにつらいよ。ってことが以下で共有されています。

grabacr.net

ところがどっこい、C++だともう少したくさんめんどい。まず、ヘッダファイルとかはWindows SDK 14393に含まれています。 デスクトップC#だと自前定義していたIRadialControllerInteropもSDKで定義があります。なので、インスタンスの取得はこういう感じ。

// IRadialControllerIntropを取得して
Microsoft::WRL::ComPtr<IRadialControllerInterop> controllerIntrop;
Windows::Foundation::GetActivationFactory(
    HStringReference(RuntimeClass_Windows_UI_Input_RadialController).Get(),
    &controllerInterop);

// HWNDに対するIRadialControllerを取得する
Microsoft::WRL::ComPtr<ABI::Windows::UI::Input::IRadialController> controller;
controllerInterop->CreateForWindow(hwnd, IID_PPV_ARGS(&controller));

イベントハンドラの追加がめんどくさめなことを除けば、だいたい同じ感じでどうにかなります。

システム定義のアイコンを消したい

Surface Dialを長押ししたときにでるメニューは、デフォルトだとシステム定義のアイコンがいくつかならんでます。

https://i-msdn.sec.s-msft.com/en-us/windows/uwp/input-and-devices/images/windows-wheel/surface-dial-menu-offscreen.png

これを消したりするには、RadialControllerConfiguration::SetDefaultMenuItemsを呼び出すことでできます。 ちなみにMSDNに特に書いてないですが、カスタムアイコンを追加せずにシステムアイコンをすべて消そうとすると、呼び出しは失敗して システム設定された状態にリセットされます。どうやら仕様だそうです。

SetDefaultMenuItemsは、.NETの世界からだとIEnumerable<RadialControllerSystemMenuItemKind>を引数として取るので、 new List<RadialControllerSystemMenuItemKind>();とか適当にやればインスタンスが作れるのですが、C++だと微妙に話が違ってきます。

C++での引数はABI::Windows::Foundation::Collections::IVector<ABI::Windows::UI::Input::RadialControllerSystemMenuItemKind>*となっています。 ちなみに似たようなの3種類あります。

  1. ABI::Windows::Foundation::Collections::IVector
  2. Windows::Foundation::Collections::IVector
  3. Platform::Collections::Vector

似たようなのがいろいろあってめんどくさいですが、1はWinRT ABIでやり取りされる型のインターフェース。2はWinRT内部でやり取りされる型のインターフェース。3は2に対する実装。です。 2が指示されている場合は、3をインスタンス化して使えばOKですが、1が指示されているばあいは、対応する型を手書きする必要があるようです。*4

今回は1が指示されているので、適当に実装するなりなんなりして使えばおっけーです。

MIDLを要求される

ABI::Windows::Foundation::Collections::IVector<T> を実装したクラスを作った。Microsoft::WRL::Make<T>経由でインスタンスも作った。 これで完璧。とおもったらそうでもなく。ABI::Windows::Foundation::Collections配下のインターフェースに対応する実装はMIDLでの定義が必要です。 プロジェクトにIDLファイルを追加して、次のようなコードが必要です。この時、名前空間はIVectorの実装をインスタンス化する位置のものを指定します。

import "inspectable.idl";
import "Windows.Foundation.idl";
import "Windows.UI.Input.idl";

#define COMPONENT_VERSION 1.0

namespace DialWin32
{
    declare{
        interface Windows.Foundation.Collections.IVector<Windows.UI.Input.RadialControllerSystemMenuItemKind>;
    }
}

これをビルドすると、たぶん失敗します。プロジェクトを右クリックして、プロパティを開いて、MIDLの設定を少し変えます。

  • C/C++ -> General -> Additional Metadata Directories
    • C:\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.FoundationContract\1.0.0.0*5
  • MIDL -> Advanced -> Prepend with ‘ABI’ namespace
    • Yes

これでビルドすると、IDLファイルのファイル名に _h.h を追加したファイルが生成されます。 このヘッダをincludeして、ABI::Windows::Foundation::Collections::IVector<T>の実装をMicrosoft::WRL::Make<T>で作ってあげれば やっとABI空間のIVector実装のインスタンスが手に入るわけです。

このあたりをやらないと

midlrt : error MIDL4034: Failed to load a dependency file. Windows.winmd (HRESULT:0x80070002 - The system cannot find the file specified. )

と言われたり、

error C2338: This interface instance has not been specialized by MIDL. This may be caused by forgetting a ‘*’ pointer on an interface type, by omitting a necessary ‘declare’ clause in your idl file, by forgetting to include one of the necessary MIDL generated headers.

と言われたりします。

それから、Prepend with 'ABI' namespaceを付けてないと、MIDLコンパイラが出力したヘッダファイルのコンパイルに失敗します。

おわりに

C++/Win32からSurface Dialを使うのは、IVectorさえ出会わなければそんなに大変じゃないです。IVectorもわかってしまえば大したことないです。 私のぐぐり力が低いばかりに、解決まで3日もかかってしまったのが非常にあれですが…

カスタムアイコンを追加するのはまだ試してないですが、たぶんWPFのつらいあれよりさらにつらそうだな…とおもいつつ、後日試してみます。

*1:http://blog.okazuki.jp/entry/2016/11/11/171706

*2:http://grabacr.net/archives/7141

*3:Windows classic samplesから引用 https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/RadialController/cs/winforms/RadialControllerInterop.cs

*4:Win2Dも手書きしてたのでたぶんそういうもんらしい

*5:たぶんもっとちゃんとした指定があるはずです…

SylphyHornのIssue #19の件

これはメモです。

TL;DR

  • SylphyHornというよりOpen.WinKeyboadHookというライブラリの問題
  • ToUnicodeExがKernel-mode Keyboard Bufferを変更するというSideEffectがある

問題

github.com

要約すると、Alt+Right押してAltを離したタイミングで♠が入力されるんだよね…というなぞ現象。

とりあえず調べてみたところ

この現象は、"Unicode対応ではないプログラムの言語"のロケールがEN-USの時に発生している。JA-JPの場合だと、Alt押しながらRight,RightのあとAlt離すで'B'が入力されるので同様の現象を確認可能。 すべての入力可能コントロールで起きるわけではなく、R#を有効にしたVisual Studioや、Explorer、Internet Explorerのアドレスバーで発生を確認。

Spy++でみてみると、SylphyHornを起動していない場合

  • WM_SYSKEYDOWN
  • WM_SYSKEYDOWN
  • WM_SYSKEYUP
  • WM_KEYUP

とメッセージが流れてくることに対して、SylphyHornを起動してると

  • WM_SYSKEYDOWN
  • WM_SYSKEYDOWN
  • WM_SYSKEYUP
  • WM_KEYUP
  • WM_CHAR

と謎のWM_CHARが出現。いったいこれはなんぞや。

原因はOpen.WinKeyboadHookにあるのでは

SylphyHornがキーフックする際に、Open.WinKeyboadHookというライブラリを使ってるのでこっちが原因じゃないの?って調べてみると、このライブラリの中のToUnicodeという関数のどこかがトリガーとなってWM_CHARが生成されてしまっているみたい。

もっと調べてみるとToUnicodeExというWin32 APIを呼び出したことがトリガーとなっていることが分かった。そこでMSDN(英語)を読んでみると、下のほうにちょろっとだけ書いてあるこれ。

As ToUnicodeEx translates the virtual-key code, it also changes the state of the kernel-mode keyboard buffer. This state-change affects dead keys, ligatures, alt+numpad key entry, and so on. It might also cause undesired side-effects if used in conjunction with TranslateMessage (which also changes the state of the kernel-mode keyboard buffer).

ToUnicodeEx function (Windows)

しれっとびっくり大事件。SideEffectとかやめてよ、これいったいどうすればいいの。ということで今日はここまで。あくまでもメモです。

Twitterのレスポンスは人が理解できるものではなくなったのだよ

TwitterのRESTとStreamingのレスポンスがあまりにも難しすぎて忘れてしまうのでメモ。

  1. RESTでtweet_mode=extendedがついていない場合
    1. 添付画像のURLを含めて140文字以下の場合
      • 今まで通りtext, entities, extended_entities が設定される
    2. 添付画像のURLを含めて140文字を超える場合
      • textには全文表示URLを含んだものが設定される。
      • truncatedフィールドがtrueになる
      • entities, extended_entitiesに画像などは含まれず、全文表示URLのみが含まれる
  2. RESTでtweet_mode=extendedがついている場合
    1. 添付画像のURLを含めて140文字以下の場合
      • textはなくなる。代わりにfull_textが増える
      • truncatedフィールドはfalseになる
      • display_text_rangeフィールドが増えて、本文範囲が明示される
      • entities, extended_entities はこれまで通り
        • 含まれるindicesはfull_textに対して正しい値が格納される
  3. Streamingの場合
    1. 添付画像のURLを含めて140文字以下の場合
      • 今まで通りtext, entities, extended_entities が設定される
    2. 添付画像のURLを含めて140文字を超える場合
      • textには全文表示URLが含まれる
      • entities, extended_entitiesに画像は含まれず、全文表示URLのみが含まれる
      • extended_tweetフィールドが増える
        • ここにfull_text, display_text_range, entities(画像入ってる)がある
        • extended_tweet.entitiesはextended_entitiesと同様に複数枚画像に対応してる

つまるところ、全部のパターンをパースしようと思うと

  1. extended_tweet フィールドを見る
  2. full_text をみる
  3. text をみる

とかしないといけなくてくそめんどい

追記: entities を見るときは

  1. extended_tweet.entities をみる (extended_tweet.entitiesはextended_entitiesを包含しているので単体でOK)
  2. 画像はextended_entities、それ以外はentitiesをみる

で、いけます。こっちもめんどい。

結論

くそめんどい

Windows 10からAndroidのテザリングをONにしてみたい

追記(10/16 11:02): もう少しちゃんと書きました。

TL;DR

背景

Windows 10 Mobileだと、Bluetoothでペアリング済みでPC、電話ともにBluetoothがONになっているとWifiテザリングの設定を端末に触ることなくONにできる機能がありますよね。 macOSiOSでもできるあれです。あれのWindows版。VAIOXperiaのセットでも独自実装されてるやつがありますね。

でも今日はWindows 10の標準実装でMicrosoft拡張のMS-TCCです。詳しいことがEngadgetに書いてました。

japanese.engadget.com

ようするにBluetoothでMS-TCCというプロファイルをサポートしていれば、Windows 10Wifi設定からテザリングを有効にできますよ。という感じ。

MADOSMAをこの機能のためだけに比較的使ってたのですが、よく考えたらこれAndroidでもできるんじゃないかなぁ…と思い、調べてたらこの仕様ってちゃんとドキュメントあるんですね*1

下調べ

MS-TCCのドキュメントを読むとやらないといけないことと、必要な情報がいくつか。

  • 必要なもの
  • やらないといけないこと
    • SDPでGUID {232E51D8-91FF-4c24-AC0F-9EE055DA30A5} を広告
    • リクエストを処理してテザリングを有効にする
  • やったほうがいいこと
    • 端末が圏外かどうかとか見てエラー返す

必要なもののうち、ESSIDとパスフレーズWifiManager#getWifiApConfigurationをリフレクションで叩くと普通に読める。問題はBSSIDで、フィールドはあるけどここに入ってない。BSSIDは通常MACアドレスと同じなのでMACアドレスを読めばいい…のだけど、Android 6.0からWifiMACアドレスが読めなくなってしまった。ので今回はなぜかまだ正しい値を読めるip addrコマンドを実行して結果からMACアドレスを探すことに。

やらないといけないことについては、BluetoothAdapter#listenUsingRfcommWithServiceRecordBluetoothソケットを作るとSDPは広告できるし、リクエストを処理してテザリングを有効にするのはWifiManager#setWifiApEnabledをこれまたリフレクションで叩くと実現できそう。

できました

そんなこんなでやってみたら動いたのでGitHubにソースとかおいておきました。

github.com

ところで、MS-TCCのドキュメントを読んでいただければわかるとおもいますが、この仕様、テザリングをONにすることはできるのにOFFにすることはできないんですよ。なのでテザリングの自動OFF機能がついてる機種以外はONになりっぱなし。最後OFFだけ手動でやってください。

いまのところAPKなしでソースだけなので各自ビルドしてください。あとBluetoothがOFFだった時のエラー処理が何も入ってないのでたぶんやばいです。 BluetoothがOffだったりOn/Offされたときにおかしくならないようにして、APKにしました。GitHubのReleasesからどうぞ*2

これできたからRobinのテザリングWindows 10からリモートでONにできるようになったのでますますMADOSMAの出番が減ってしまいそうだなーって…