tmytのらくがき

個人の日記レベルです

Desktop Bridgeで既存のWin32アプリを手軽にパッケージングしたい

既存のWin32アプリをWindows Storeで公開したりWindows 10Sで実行するには、Desktop BridgeでUWPに見せかけるしかないんですが、ぱっとみすごくめんどくさそうに見えるんです。でもMSDNに裏技が書いてあったので試してみました。

※今回やったことはMSDNに全部書いてあります。Visual Studio による .NET デスクトップ アプリ用デスクトップ ブリッジ パッケージ ガイド - UWP app developer | Microsoft Docsのステップ1 変換の個所が該当します。

そもそも

そもそもミスリードしてる人が多いですが、Win32アプリをUWPに変換するためにDesktop App Converter (DAC)は必須要件では『ありません』。Desktop App Converterはインストーラが存在する既存のWin32アプリをある程度自動で変換するための仕組みです。

DACもやってることは単純で仮想マシン起動して、その中でインストーラ実行して、インストーラが終了した時点でのファイルシステムレジストリの差分を回収するだけの仕組みです。Desktop Bridge自体はDAC非依存なので、UWPに含めるパッケージを自力で作れるのであれば、DACなんて使わなくてもいいんです。

結局はDesktop Bridgeで変換されたWin32アプリは、Appxの中にWin32の実行ファイルが入ってるだけのたんなるAppxパッケージです。

なので、たんにZIPしてるだけのアプリをUWPにしたいがためだけにインストーラ書くとか完全に間違いで、MSDN見たらもっと簡単な方法があったのでやってみました。今回はコードスニペットと、画像多めでお送りします。

準備

まず変換したいアプリを用意します。

MSDNで紹介されているのは、.NETアプリなのでWPFとかのが該当します。が、手元にそんなものはなかったのでAzureaを変換してみます。MSDNにあるのと、今回試したのの大きな違いは.NETかC++ Nativeかです。MSDNでは.NETアプリなので、Any CPUでおっけー!*1とか書いてますけど、これ残念ながらネイティブコードなのでx86とx64で実行コード違うし、そもそもAnyにしたらx86で詰むやろ…とかあるのでその辺も回避してきます。

この時点でソリューションはまだこうです。これからいろいろやっていきます。

f:id:tmyt:20170509004204p:plain

JavaScript UWPプロジェクトの追加

何度も言いますけど、Desktop Bridgeで変換されたWin32アプリは、Appxの中にWin32の実行ファイルが入ってるだけのたんなるAppxパッケージです。なので、簡単にパッケージを作るには、UWPプロジェクトにファイル追加してマニフェスト編集するのが一番楽なんです。ストア用の署名とかも全部勝手にやってくれるし。

ということで、まずはJavaScript UWPプロジェクトを追加します。よーするにJavaScript UWPプロジェクトをいけにえにして、Desktop Bridgeで実行されるWin32アプリを含んだAppxを作ります。なんでJavaScriptなのかというと、最初のMSDNのページの一番最後にちらっと書いてあります。

  • Debug でアプリをビルドすると、次のようなエラーが出力されます。Microsoft.Net.CoreRuntime.targets(235,5): エラー: カスタム エントリ ポイントを持つアプリケーションの実行可能ファイルはサポートされていません。 パッケージ マニフェストでの Application 要素の Executable 属性を確認してください。 この問題を回避するには、代わりに Release モードを使用します。
  • UWP プロジェクトのルート フォルダーに格納されている Win32 バイナリは、Release では削除されます。 Win32 バイナリを格納するフォルダーを使用しない場合、.NET Native コンパイラは最終的なパッケージからそれらのバイナリを削除します。これにより、実行可能ファイルのエントリ ポイントが見つからないため、マニフェストの検証エラーが出力されます。

ということなので全く話になりません。JavaScript UWPプロジェクトは単なるHTMLを表示するだけのマニフェストでビルドもなにもしないから余計なこと何もしないので、試してみたらたまたまうまくいきました。とかそういう感じなんでしょう。きっと。

f:id:tmyt:20170509004751p:plain

普段C#とかVBでUWP書いてる人はもしかするとインストールすらしてないかもしれないJavaScriptを選択して、Blank Appを作成します。名前は『アプリの名前.Package』とかにしとくと分かりやすいと思います。今回変換したいのはAzureaなのでAzurea.Packageという名前でプロジェクトを作りました。

プロジェクト作ったときに聞かれるターゲットバージョンですが、どうせ後で書き換えちゃうのでお好きな感じで結構です。

f:id:tmyt:20170509005623p:plain

プロジェクトが出来上がると、ソリューションがこんな感じになります。

f:id:tmyt:20170509005704p:plain

そしてVisual Studioでは自動的にmain.jsが開かれて

// Your code here!

f:id:tmyt:20170509005742p:plain

とか言ってますが、このファイルが入ってるディレクトリを含め、赤丸の部分は不要なので削除します。

f:id:tmyt:20170509005907p:plain

すっきりしました。

f:id:tmyt:20170509010009p:plain

imagesには、アプリのアイコンとかが入ってたりするのでそのまま置いておきます。(あとで使います)

Win32実行ファイルの追加

いけにえにするJavaScript UWPプロジェクトの準備ができたので、Appxに含めるファイルを追加していきます。

MSDNの手順にそって、win32というフォルダを作ります。

f:id:tmyt:20170509010254p:plain

別にこれwin32である必要はまったくなくて、わかりやすそうだからwin32なだけです。appとか、applicationとかprogramとかなんでも大丈夫です。なんで何でもいいかというと、Win32の実行ファイルがAppxにパッケージングされていることが大事で、実行するWin32実行ファイルのパスはappxmanifestで指定するので本当になんでもいいです。なんでもいいんですけど、MSDNでwin32って書いてたので、とりあえずそのままwin32で進めます。

次に、アプリケーションが実行に必要なファイルをwin32の中にいい具合に追加します。とりあえず、Releaseビルドした結果をエクスプローラからドラッグ&ドロップで追加してみました。

f:id:tmyt:20170509010751p:plain

この時pdbももれなく追加するのがポイントです*2pdbを追加しておくと、Appxのパッケージングプロセスの中でpdbだけ切り出されていつものUWPみたいにappxsymにしてくれます。ぜひ追加しましょう。

ファイルを追加したら、win32配下のファイルをすべてコンテンツとしてマークします。コンテンツにしないとAppxのなかにうまく含めてくれません。

f:id:tmyt:20170509011159p:plain

出力ディレクトリにコピーについても新しければコピーするもしくは、常にコピーするにします。

f:id:tmyt:20170509011243p:plain

ここまで来たら基本的にはだいたい終わりです。あらまぁ簡単ですね。って感じです。

appxmanifestの編集

パッケージのコンテンツにWin32実行ファイルの実行に必要なファイルを追加したんですが、手動でコピーしました。これだと、毎回コピーしないといけなくて面倒なんですが、それの対応はちょっと後回しにしてappxmanifestを編集します。

残念ながらDesktop Bridge周りはUIで編集できないので手作業で編集します。まずはpackage.appxmanifestを右クリックして、コードを表示します。

f:id:tmyt:20170509011536p:plain

するとこういうXMLが表示されます。

<?xml version="1.0" encoding="utf-8"?>
<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  IgnorableNamespaces="uap mp">

  <Identity
    Name="0f891c8e-ba95-4518-b6ab-b73741a82090"
    Version="1.0.0.0"
    Publisher="CN=yutaka" />

  <mp:PhoneIdentity PhoneProductId="0f891c8e-ba95-4518-b6ab-b73741a82090" PhonePublisherId="00000000-0000-0000-0000-000000000000" />

  <Properties>
    <DisplayName>Azurea.Package</DisplayName>
    <PublisherDisplayName>yutaka</PublisherDisplayName>
    <Logo>images\storelogo.png</Logo>
  </Properties>

  <Dependencies>
    <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
  </Dependencies>

  <Resources>
    <Resource Language="x-generate" />
  </Resources>

  <Applications>
    <Application 
      Id="App"
      StartPage="index.html">

      <uap:VisualElements
        DisplayName="Azurea.Package"
        Description="Azurea.Package"
        BackgroundColor="transparent"
        Square150x150Logo="images\Square150x150Logo.png"
        Square44x44Logo="images\Square44x44Logo.png">

        <uap:DefaultTile Wide310x150Logo="images\Wide310x150Logo.png" />
        <uap:SplashScreen Image="images\splashscreen.png" />

      </uap:VisualElements>
    </Application>
  </Applications>

  <Capabilities>
    <Capability Name="internetClient" />
  </Capabilities>

</Package>

MSDNに書いてある内容を順番に反映していきます。

まずPackageタグにxmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"を追加します。さらに、PackageタグのIgnorableNamespacesrescapも追加します。IgnorableNamespacesはスペース区切りなので、uap mp rescapになります。

追加したPackageタグはこうなります。

<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
  IgnorableNamespaces="uap mp rescap">

次にDependenciesタグにあるTargetDeviceFamily<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.351" MaxVersionTested="10.0.14393.351" />で置き換えます*3。置き換えたDependenciesはこうなります。

  <Dependencies>
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.351" MaxVersionTested="10.0.14393.351" />
  </Dependencies>

Capabilitesタグに<rescap:Capability Name="runFullTrust" />を追加します。これでAppxが完全信頼で実行されるようになります*4。追加するとこうなります*5

  <Capabilities>
    <rescap:Capability Name="runFullTrust" />
    <Capability Name="internetClient" />
  </Capabilities>

最後にアプリのエントリポイントを設定します。ApplicationタグにExecutable属性とEntryPoint属性を指定します。この時、Executable属性には、実行ファイルのAppx内の相対パス*6EntryPoint属性にはWindows.FullTrustApplicationを指定します。今回変換するAzureaはAzurea.exeが実行ファイル名なので、これらを設定するとこうなりました。

<Application Id="Azurea" Executable="win32\Azurea.exe" EntryPoint="Windows.FullTrustApplication">

これで手書きで編集しないといけない部分はおしまいです。あとアイコンとかはいつも通りGUIから生成とかしちゃって大丈夫です。

ここまで全部変更したXMLはこうなります。

<?xml version="1.0" encoding="utf-8"?>
<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
  IgnorableNamespaces="uap mp rescap">


  <Identity
    Name="0f891c8e-ba95-4518-b6ab-b73741a82090"
    Version="1.0.0.0"
    Publisher="CN=yutaka" />

  <mp:PhoneIdentity PhoneProductId="0f891c8e-ba95-4518-b6ab-b73741a82090" PhonePublisherId="00000000-0000-0000-0000-000000000000" />

  <Properties>
    <DisplayName>Azurea.Package</DisplayName>
    <PublisherDisplayName>yutaka</PublisherDisplayName>
    <Logo>images\storelogo.png</Logo>
  </Properties>

  <Dependencies>
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.351" MaxVersionTested="10.0.14393.351" />
  </Dependencies>

  <Resources>
    <Resource Language="x-generate" />
  </Resources>

  <Applications>
    <Application
      Id="Azurea"
      Executable="win32\Azurea.exe"
      EntryPoint="Windows.FullTrustApplication">

      <uap:VisualElements
        DisplayName="Azurea.Package"
        Description="Azurea.Package"
        BackgroundColor="transparent"
        Square150x150Logo="images\Square150x150Logo.png"
        Square44x44Logo="images\Square44x44Logo.png">

        <uap:DefaultTile Wide310x150Logo="images\Wide310x150Logo.png" />
        <uap:SplashScreen Image="images\splashscreen.png" />

      </uap:VisualElements>
    </Application>
  </Applications>

  <Capabilities>
    <rescap:Capability Name="runFullTrust" />
    <Capability Name="internetClient" />
  </Capabilities>

</Package>

この状態でAppxmanifestのGUIエディタを開くとスタートページが設定されてない。とか言ってきます。

f:id:tmyt:20170509013058p:plain

このパラメータは本来Applicationタグに設定されているんですが、さっき消しちゃったのでこんなエラーがでます。出るんですが、Appx作るうえで何も起こられないのであまり気にせずこのまま放っておきます。

パッケージのビルド

あとはもうビルドしたらできあがりです。プロジェクトを右クリックして、ストア、アプリパッケージの作成でできます。

f:id:tmyt:20170509013305p:plain

この先は普段のUWPのビルドと変わらないので割愛します。ひとつだけ注意する点があって、パッケージのアーキテクチャの指定です。

f:id:tmyt:20170509013420p:plain

.NETでWPFみたいなILだけのアプリの場合はNeutralにAny CPUを指定します。これでOKです。それ以外の人は、x86とx64にチェックを入れて、それぞれビルドしましょう。しばらくするとパッケージが出力されます。インストールとかして試してみるといいと思います。

パッケージに含めるコンテンツを自動で更新する

ここまででできたAppxの中身を更新するには毎回手作業でコピーしてあげないといけなくてかったるいので自動でやります。そもそも、今回のAzureaはネイティブコードなのでx86/x64のパッケージを別々に生成しないといけなくて毎回コピーなんてめんどくさくてやってられません。

これらを実現するためにスクリプトを書きます。今時ならPowerShell使えよって感じなんですが、PowerShell力が低いのでバッチファイル書きます。

まず、win32の中身をクリーンするやつです。これをcleanup.batとします。終了コードが0じゃないとビルドが止まってしまうのでとりあえずexit 0してます。

del /Q win32\Azurea.exe
del /Q win32\Azurea.pdb
del /Q win32\AzLang.dll
del /Q win32\AzLang.pdb
del /Q win32\ColorSchemes\*
del /Q win32\Language\*
exit 0

次に、プロジェクトの出力を集めてwin32ディレクトリにコピーするやつです。これはcopyfiles.batとします。Appxパッケージビルド時のプラットフォーム名を使ってコピーするディレクトリを振り分けています。

@echo off
echo Packaging for %*
set build=%1%
set arch=%2%
mkdir win32

if "%arch%"=="x64" goto copy_amd64
if "%arch%"=="x86" goto copy_x86
goto undefined_platform

:copy_amd64
copy ..\x64\%build%\Azurea.exe win32\Azurea.exe
copy ..\x64\%build%\Azurea.pdb win32\Azurea.pdb
copy ..\x64\%build%\AzLang.dll win32\AzLang.dll
copy ..\x64\%build%\AzLang.pdb win32\AzLang.pdb
goto copy_noarch

:copy_x86
copy ..\%build%\Azurea.exe win32\Azurea.exe
copy ..\%build%\Azurea.pdb win32\Azurea.pdb
copy ..\%build%\AzLang.dll win32\AzLang.dll
copy ..\%build%\AzLang.pdb win32\AzLang.pdb
goto copy_noarch

:copy_noarch
mkdir win32\ColorSchemes
mkdir win32\Language
xcopy ..\Azurea\ColorSchemes win32\ColorSchemes /-Y /E
xcopy ..\Language win32\Language /-Y /E
goto EOF

:undefined_platform
echo "Undefined platform " %arch%
exit 1

:EOF

これらはプロジェクトの中のscriptsというディレクトリにまとめて保存しました。

f:id:tmyt:20170509014256p:plain

次にこれらを呼び出してもらわないといけません。ということでまずJavaScript UWPプロジェクトをアンロードします。

f:id:tmyt:20170509014350p:plain

次に、アンロードしたプロジェクトを右クリックして、編集を選択します。

f:id:tmyt:20170509014428p:plain

表示されたXMLを下のほうまでスクロールして、最後にこんなのを追加します。

  <Target Name="BeforeBuild">
    <Exec Command="scripts\cleanup.bat" />
    <Exec Command="scripts\copyfiles.bat $(Configuration) $(Platform)" />
  </Target>
  <PropertyGroup>
    <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
  </PropertyGroup>

追加したら保存して閉じます。それぞれの意味ですが、

  • BeforeBuild ビルド前に実行するタスクです。
  • $(Configuration) ビルド設定で、ReleaseとかDebugとか入っています。
  • $(Platform) アーキテクチャx86とかx64とか入ってます。

これでAppxのビルド時に自動でコピーされるようになっていい感じになりました。

自動で追加できるようにすると、コピーするファイルをコンテンツでマークするのが面倒になります。その場合は、同じように、プロジェクトをアンロードして、編集して、Contentタグにワイルドカードを指定してあげるといい感じになります。

Azureaではこんな感じで指定してます。

  <ItemGroup>
    <Content Include="win32\Azurea.exe">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="win32\AzLang.dll">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="win32\Azurea.pdb">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="win32\AzLang.pdb">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="win32\ColorSchemes\*.txt">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="win32\Language\*.*">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

おしまい

MSDNでは.NETアプリのみを対象としてJavaScript UWPプロジェクトをいけにえにしてDesktop BridgeなAppxを作る方法が紹介されてましたが、普通のNativeアプリケーションでも工夫すればどうってことなかったです。

今回のエントリはパッケージを作成するところまでですが、署名鍵をストア申請用に変更するためにパッケージと予約名を関連付けるとかは、通常のUWOとおんなじです。いい記事もたくさんあるので参考にしてみるといいと思います。

ちなみに、ですが、現状Desktop Bridgeを使ったFull TrustなAppxは通常の方法ではストアに提出できません。だそうです。Full TrustなAppxが提出できるフラグをアカウントにつけてもらわないといけないので、Desktop Bridge を活用して、既存のアプリやゲームを Windows ストアに移行しましょうのフォームを送信する必要があるそうです*7

*1:実行コードはILで書いてあってCLRで実行されるので

*2:MSDNに書いてないけど

*3:MSDNに10.0.14393.0って書いてますが、どうもDesktop BridgeにバグがあってWindowsBSODすることがあるらしく、この問題が修正された10.0.14393.351を指定するほうがよさそうです。

*4:Run Full Trust あきらかにパワーがある

*5:ちなみにrunFullTrustなアプリはAppContainerで実行されないのでinternetClient capabilityとか書いても書かなくてもあんまり意味はありません。

*6:なのでプロジェクトに作ったwin32というディレクトリ名はなんでもよかったのです。

*7:先日やったばかりなので実際どうなってるのかはよくわかりません

今話題のMastodonインスタンスを建ててS3の代わりにAzure Blob Storageを使う

Pixivとかドワンゴインスタンスを建てて話題沸騰のMastodonですけど、僕もAzure IaaS上にインスタンスを建ててみてました。 Docker Composeでさくっと作っただけだったのを、画像アップロード先をAzure Blobにするとかしてちょっとカスタムしたりもしたのでそのお話です。

Azure Blobにする

Mastodonは標準でAmazon S3に対応してるので、S3使う人はそのまま使えばなんとなくうまくいきます。でもAzure IaaSに建てたしせっかくなのでBlob使います。

Azure BlobをS3のAPIでアクセスするには s3proxy っての使えばどうにかなります。Java製。

github.com

docker-compose.ymlにs3proxy追加しました。

diff --git a/docker-compose.yml b/docker-compose.yml
index 81c6fe9..d953d44 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,6 +15,11 @@ services:
 #    volumes:
 #      - ./redis:/data

+  s3proxy:
+    restart: always
+    image: andrewgaul/s3proxy
+    env_file: .env.production
+
   web:
     restart: always
     build: .
@@ -26,6 +31,7 @@ services:
     depends_on:
       - db
       - redis
+      - s3proxy
     volumes:
       - ./public/assets:/mastodon/public/assets
       - ./public/system:/mastodon/public/system

そして、.env.productionにS3といいつつAzure Blobにアクセスするように設定します。します。

 # S3 (optional)
-# S3_ENABLED=true
-# S3_BUCKET=
-# AWS_ACCESS_KEY_ID=
-# AWS_SECRET_ACCESS_KEY=
-# S3_REGION=
-# S3_PROTOCOL=http
-# S3_HOSTNAME=192.168.1.123:9000
+S3_ENABLED=true
+S3_BUCKET=uploads
+AWS_ACCESS_KEY_ID=local-identity
+AWS_SECRET_ACCESS_KEY=local-credential
+S3_PROTOCOL=https
+S3_HOSTNAME=***.blob.core.windows.net
+S3_ENDPOINT=http://s3proxy/
+S3_PERMISSION=private

それに加えて、s3proxyの設定も書きます。。

+S3PROXY_AUTHORIZATION=none
+S3PROXY_CORS_ALLOW_ALL=true
+JCLOUDS_PROVIDER=azureblob
+JCLOUDS_ENDPOINT=https://***.blob.core.windows.net/
+JCLOUDS_IDENTITY=***
+JCLOUDS_CREDENTIAL=****************************************

だいたいこれでいいはずなんですけど、このまま動かすとs3proxyから501 Not Implementedエラーが帰ってくるのです。なんか、調べたところによると、X-Amz-Aclヘッダにpublic-readが指定されてるとなんかよくわかんないけどAzure Blob宛てだと501 Not Implementedになるみたいです。

で、このパラメータなんですけど、config/initializers/paperclip.rbpublic-readに決め打ちされてて非常に困るので適当にパッチします。

diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index 77bc13b..6fab071 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -16,7 +16,7 @@ if ENV['S3_ENABLED'] == 'true'
   Paperclip::Attachment.default_options[:s3_host_name]   = ENV.fetch('S3_HOSTNAME') { "s3-#{ENV.fetch('S3_REGION')}.amazonaws.com" }
   Paperclip::Attachment.default_options[:path]           = '/:class/:attachment/:id_partition/:style/:filename'
   Paperclip::Attachment.default_options[:s3_headers]     = { 'Cache-Control' => 'max-age=315576000' }
-  Paperclip::Attachment.default_options[:s3_permissions] = 'public-read'
+  Paperclip::Attachment.default_options[:s3_permissions] = ENV.fetch('S3_PERMISSION') { 'public-read' }
   Paperclip::Attachment.default_options[:s3_region]      = ENV.fetch('S3_REGION') { 'us-east-1' }

これでdocker-compose buildしてdocker-compose up -d するといい感じになりました。

14393のトーストの挙動メモ

Windows 10 14393のトーストの挙動、特に画像周りのメモ。

  • <image placement=“inline” /> が1個
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305130149p:plain

f:id:tmyt:20170305130255p:plain

f:id:tmyt:20170305130331p:plain

  • <image placement=“inline” /> が複数個
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305130427p:plain

f:id:tmyt:20170305130515p:plain

f:id:tmyt:20170305130526p:plain

  • <image placement=“hero” /> が1個
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305130634p:plain

f:id:tmyt:20170305130721p:plain

f:id:tmyt:20170305130735p:plain

  • <image placement=“hero” /> が複数個
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305130826p:plain

f:id:tmyt:20170305130944p:plain

f:id:tmyt:20170305130955p:plain

  • <image placement=“inline” /> と <image placement=“hero” /> の組み合わせ
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305131156p:plain

f:id:tmyt:20170305131258p:plain

f:id:tmyt:20170305131312p:plain

FIDO 2.0を実装してみた感じ

TL;DR

  • Passport.jsでFIDO 2.0実装してみた
  • EdgeでWindows Helloした
  • あとあとよく見たらpassport-mspassport*1ってのがありました

やりたかったこと

WebサービスWindows Hello認証をやってみたくていろいろ試してみた感じ。Windows HelloはFIDO 2.0の実装となっていて、公開鍵暗号をベースとしたチャレンジレスポンス認証。

あくまでもユーザとデバイスの組の確実性しか保証されないので、ログインしようとしているユーザがリソースにアクセスする権限があるかどうかについては関与できない。なので、実際に使うとなると既知のユーザアカウント認証に対して公開鍵を登録するという実装になるのかな。という感じ。 今回実装したものも、GitHubOAuth認証がすでに実装されているのでそこに対してFIDO 2.0の公開鍵を登録する仕組みにしました。

GitHub認証はPassport.jsで実装しているのでWindows Hello認証もそこに混ぜてみることにしました。

用意したもの

  • express.js
  • passport.js
  • passport-fido2*2
  • webauthn.js

実装

Passport.jsのStrategyを作るには、Passport-strategyを継承してauthenticate関数を実装すればいいらしい。 そんでもって、Windows HelloとかFIDO2.0とかでぐぐるとNodeな実装がちらほら見つかるのでいろいろ参考にしながら実装したものがこちら。

github.com

サーバが送信したChallengeであることを検証するためにChallengeをHMAC-SHA256したものをクライアントから送信させているのに、 検証に使っていない…のはそのうち修正するとして…

このStrategyは送信されたIDに対応するサーバにすでに格納されている公開鍵を使ってリクエストを検証するところまでしか面倒を見ません。 なので実際に使うには既知のアカウントでログインし、公開鍵を保存する必要があります。 その時のクライアント実装はこういう感じにしてます。

const p = this.context.profile;
const account = { rpDisplayName: p.username, displayName: p.username, imageUri: p.avater };
const cryptoParams = [{ type: 'FIDO_2_0', algorithm: 'RSASSA-PKCS1-v1_5' }];
navigator.authentication.makeCredential(account, cryptoParams)
.then(result => {
  const key = JSON.stringify(result.publicKey);
  const id = result.credential.id;
  Http.post('/api/credentials/fido/register', {key, id},
    () => Toast.show('FIDO key registered', 'success'),
    () => Toast.show('Key registration failed', 'warning')
  );
  console.log(result);
});

認証済みエリアに証明書登録ボタンをつくって、最初にこれで登録。そのあとログイン画面に移ると

sign(challenge){
  const q = qs.parse(this.context.router.location.search.substring(1));
  navigator.authentication.getAssertion(challenge.c)
  .then(result => {
    const uri = '/auth/fido2?'
      + `c=${challenge.c}&cs=${challenge.cs}&`
      + `authenticatorData=${result.authenticatorData}&`
      + `clientData=${result.clientData}&`
      + `signature=${result.signature}&`
      + `id=${result.credential.id}`
      + (q._redir ? `&_redir=${encodeURIComponent(q._redir)}` : '');
    window.location.href = uri;
  });
}
authenticate(){
  Http.get('/auth/fido2/challenge', {},
    c => this.sign(c),
    () => Toast.show('Failed to exchange challenge', 'error')
  );
}

ってやってます。最初にサーバからチャレンジをもらって、それに対して秘密鍵で署名。そのあと、passport-fido2を呼び出しています。

Edgeのfido2周りの実装が結構標準から離れてるらしく、Webauthn.jsを使ったほうがいいよってid:shibayan に教えてもらったりしたので使ったほうがよさそうです。 最新のドラフトと見比べてみてもRequiredな引数がRequiredじゃなかったりするし、もう少し変更ありそうな気もしたり。

まとめ

よくよく探したらpassport-mspassport ってのがありました。

github.com

ブラウザで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個認識できないのかなぁ…ということでやってみました。

TL;DR

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

バイス構成

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;
  sbyte Degree;
  byte Orientation;
  byte[6] Nazo;
}

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

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

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

4バイト目は回転方向で、右回転なら00、左回転ならFFが入っています。

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

まとめ

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より下では別々のものにみえてました。