自宅kubernetesクラスタでTwitchのチャット読み上げツールを自作した話

概要

4年と9ヵ月働いたヤフーを退職して暇になったので、新しい盆栽を初めたという話。

システム構成

用意したもの

kubernetes、 CoreDNS+etcd

kubeadmを使って雑に構築、ingress-nginx-controller, flannel, metallb, external-dnsあたりを採用しています。以下を参考にして構築しました。

nnstt1.hatenablog.com

wiki.archlinux.jp

以下雑記

  • ingress-nginx-controllerについては、別でrtmpコンポーネントが動いていたりするので、tcp-services周りの設定を入れていますが、それ以外はほぼバニラです。
  • external-dnsでは *.mystic.doll を良い感じに解決するように設定しています。

TwitchとOAuthで認証するやつ

Rustで書きました。axum で雑に書きました。

今まではTwitchのAccessTokenを取るのに以下のシェルスクリプトでやっていたんですが、こちらをkubernetes上に移し、ついでにkubernetesのSecretとして保存するようにしました。

#/bin/bash

echo access "https://id.twitch.tv/oauth2/authorize?client_id=${TWITCH_CLIENT_ID}&redirect_uri=http://localhost:8000/twark/&response_type=code&scope=chat:read+chat:edit+channel:read:redemptions"

CODE=$(nc -lp8000 | head -1 | cut -d'?' -f2 | cut -d'=' -f2 | cut -d'&' -f1)
export TWITCH_ACCESS_TOKEN=$(curl -XPOST "https://id.twitch.tv/oauth2/token?client_id=${TWITCH_CLIENT_ID}&client_secret=${TWITCH_CLIENT_SECRET}&code=${CODE}&grant_type=authorization_code&redirect_uri=http://localhost:8000/twark/" | jq -r '.access_token')

kubernetesAPIを叩くクライアントのcrateもあるにはあるが、ドキュメントを読んで面倒になったので、自分でリクエスト投げることにしました。

github.com

kubernetesAPIでSecretがなければ登録、あれば更新、というのが見つからなかったので自前で実装した結果可読性が最惡になっています。

voicevoxのあれこれ

解説が面倒なのでmanifestsを全部まとめて下記に置いておきます。 大体以下のことをしています。

  • kubernetes上にpod配置してServiceでクラスタ内、ingressクラスタ外からアクセスできるようにしてます。
  • Default設定ではAccess-Control-Allow-OriginとかCORS周りの設定が厳しめなので、これを緩くしています。
    • 適当に試していたときユーザー設定のところに置いた状態で初回起動したとき設定が反映されなくてキレたのでdefaultを置き換えています。
    • そもそも初回起動時に設定をを反映させる手段がundocumentedだったのでコード読んでたんですがpythonのappdirsのデフォルトに配置されるっぽいがうまくいかなかった
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: voicevox
  labels:
    app: voicevox
spec:
  replicas: 1
  selector:
    matchLabels:
      app: voicevox
  template:
    metadata:
      labels:
        app: voicevox
    spec:
      containers:
      - name: voicevox
        image: voicevox/voicevox_engine:cpu-ubuntu20.04-latest
        ports:
        - containerPort: 50021
        volumeMounts:
        - name: default-setting
          mountPath: /opt/voicevox_engine/default_setting.yml
          subPath: default_setting.yml
      volumes:
      - name: default-setting
        configMap:
          name: voicevox-config
          items:
          - key: "default_setting.yml"
            path: "default_setting.yml"
---
apiVersion: v1
kind: Service
metadata:
  name: voicevox
  annotations:
    external-dns.alpha.kubernetes.io/hostname: voicevox.mystic.doll.
spec:
  selector:
    app: voicevox
  ports:
  - name: voicevox
    protocol: TCP
    port: 80
    targetPort: 50021
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: voicevox
spec:
  ingressClassName: nginx
  rules:
  - host: voicevox.mystic.doll
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: voicevox
            port:
              number: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: voicevox-config
data:
  default_setting.yml: |
    allow_origin: "*"
    cors_policy_mode: all

Twitchのチャットを拾ってきてvoicevoxのクエリに変更するWebSocketサーバ

やりたいこととしては WebSocket<TwitchChat> -> WebSocket<VoiceVoxQuery> というマッピングを実現するサーバです。

通常読み上げ自体をやりたければ、フロントエンド側でチャットに接続→チャットの内容をvoicevoxに投げる、というだけで良いです。 ですが、これを実装しようと思うとフロントエンド側にAccessTokenを渡す必要があり、Secretsに渡した意味が皆無になってしまうので、こういった実装になりました。

tokio-tungstenite でChatに接続、Message::Text を受け取る度にvoicevoxにリクエストして、今自分が喋ってるWebSokectサーバに繋いできてるpeerにvoicevoxの音声合成クエリをbroadcastしています。

詳しくはリポジトリのコードを見てください。

github.com

雑にWebAudio APIで読み上げするフロントエンド

雑にnginxで一式ホストしています。Manifestとしては以下。

真面目にアプリケーションを書く必要が0なので、雑にFetchしてきてblobをWebAudio APIに投げるようなコードを書いています。

---
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: twitch-tools
  name: tts-html
data:
  index.html: |
    <html>
      <body>
        <script>
          const connectWebSocket = async () => {
            const socket = new WebSocket("ws://tc2vv.mystic.doll");
            socket.addEventListener("message", async (e) => {
              const data = JSON.parse(e.data);
              if (data.kana.length < 40) {
                const ctx = new AudioContext();
                const res = await fetch("http://voicevox.mystic.doll/synthesis?speaker=1", {method:"POST", body: e.data, headers: {"Content-Type": "application/json"}});
                const arrayBuffer = await res.arrayBuffer();
                const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
                const source = ctx.createBufferSource();

                source.buffer = audioBuffer;
                source.connect(ctx.destination);
                source.start();
              }
            });
            socket.addEventListener("close", () => {
              connectWebSocket();
            });
          };

          connectWebSocket();
        </script>
      </body>
    </html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tts
  namespace: twitch-tools
spec:
  replicas: 2
  selector:
    matchLabels:
      app: tts
  template:
    metadata:
      labels:
        app:
          tts
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
        volumeMounts:
        - name: index-html
          mountPath: /usr/share/nginx/html/index.html
          subPath: index.html
      volumes:
      - name: index-html
        configMap:
          name: tts-html
          items:
          - key: "index.html"
            path: "index.html"
---
apiVersion: v1
kind: Service
metadata:
  name: tts
  namespace: twitch-tools
spec:
  selector:
    app: tts
  ports:
  - name: tts
    protocol: TCP
    port: 80
    targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tts
  namespace: twitch-tools
spec:
  ingressClassName: nginx
  rules:
  - host: tts.mystic.doll
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: tts
            port:
              number: 80

まとめ

普通に棒読みちゃんとか使ったほうがいいです。

あとうちみたいな場末の配信には別に読み上げいらないです。

令和最新版、全国1億2000万人のサンリオワールドスマッシュボーラーに贈るネトスマ環境導入ガイド

はじめに

エバリブー
みなさん大人気e-sports対戦ゲーム、サンリオワールドスマッシュボール!を楽しんでいる頃と思います。

一人用モードのRTAや友達との対戦等、様々な遊び方がありますが、今回はエミュレータを用いたネットワーク対戦、通称「ネトスマ*1」環境の導入方法をお伝えしていきます。

この記事の内容を実践する際は全て自己責任で行ってください。

追記

RetroFreakDumperの公開が終了しているようです。

そのため、吸い出しについてはまた別の方法をお試しください。

必要なもの

吸い出し

RetroFreakDumper をダウンロードし、起動します。このとき、COMポートの一覧に表示されている COM<数字> を覚えておきます。

RetroFreakDumperの画面

次にレトロフリークカートリッジアダプターにカセットを挿入し、PCに接続します。

カートリッジアダプター

その後RetroFreakDumperを再起動します。先程起動したときにCOMポートの一覧に表示されていなかった COM<数字> がカートリッジアダプターに対応する COM<数字> になるので、これを選択し、選択ボタンを押します。

RetroFreakDumperの画面(カートリッジアダプタ接続後)

選択ボタンを押すと、カセットの情報と吸い出しボタンが出るので、吸い出しボタンを押して吸い出しを開始します。設定は特に弄らなくて大丈夫です。

吸い出し前

吸い出しが完了したら、RetroFreakDumperのあるフォルダ内の Roms\SFC フォルダに Sanrio World Smash Ball!.sfc というファイルが出力されます。これで吸い出しは完了になります。

対戦用パッチ

SWSBのコミュニティでは通常のROMでは2P側が不利となるバグが存在するため、有志により作成頂いた対戦用パッチを利用して対戦しています。

対戦用パッチをこちらのdiscordのリンクからダウンロードします。(事前にdiscordに参加しておいてください)

WinIPSを実行し、IPSファイル に対戦用パッチ swsb_tournament.ips を、 パッチするファイルに先程吸い出した Sanrio World Smash Ball!.sfc を指定し、適用ボタンを押します。

WinIPS

パッチが成功すれば、対戦用パッチのフォルダ内に swsb_tournament.sfc というファイルが出力されているはずです。

Fightcade2の準備

先程パッチした swsb_tournament.sfc をZIP圧縮し、smashbal.zip として保存しておきます。

https://www.fightcade.com/register からユーザー登録し、 https://www.fightcade.com/からFightcade2をダウンロードしインストールしてください。その後先程作成した smashbal.zip<Fightcadeのインストール先フォルダ>\Fightcade\emulator\snes9x\ROMs フォルダに移動します。

Fightcade2のユーザー登録画面

インストール先フォルダが分からない場合は、Fightcadeのショートカットを右クリックし、ファイルの場所を開く→ ROMs フォルダ → SNES9x ROMs のショートカット の順でアクセスすればROMsフォルダに到達できます。

ファイルの場所を開く
ファイルの場所を開く、で開いたフォルダ
SNES9x ROMsのショートカットからROMsフォルダへ飛べる
smashbal.zipをこちらに配置する(変なROMをここに置いてるけど気にしないで)

Fightcade2を起動したら先程登録した内容でログインし、左の検索アイコンから SANRIO WORLD SMASH BALL! を検索し、+JOINを押します。

Fightcade2の検索画面

Fightcade2起動確認

左側にチャンネルが追加されているので、こちらをクリックし、右上のTEST GAME からゲームを起動します。

Fightcade2のSWSBチャンネルの画面(TEST GAMEを押してね)

SNES9xが起動するので、InputInput Configuration から入力設定を変更してください。斜め入力の設定も忘れずにしておきましょう。

Input → Input Configuration
キーコンフィグ

また、EmulationPause When Inactive のチェックもはずしておきましょう。

`Emulation` → `Pause When Inactive` のチェックを外しておく

設定が終わったらSNES9xを終了して大丈夫です。

対戦

左下のアイコンをクリックにし、自分のログイン状態を Online にしてください。

自分のログイン状態をオンラインにする

オンラインのユーザー名を選択すると対戦を申し込むことができます。対戦を申し込んだ側のプレイヤーが1Pとなります。

まとめ

以上でネトスマ環境導入は完了です。 不明な点等あればSWSB discordや、私宛てにTwitter等でリプライして頂ければ対応しますので、ご気軽にご相談ください。

全人類ネトスマやりましょう。

*1:ネトスマの「スマ」はスマッシュボールの「スマ」です

how to execute the binary in Windows host from the process in WSL2 guest VM

背景

最近久々に視聴者が操作を注入できるFall Guysを配信*1 でやろうとしていたのだが、久々にやろうとしたらbotのコードを一切変えてないのに操作を注入するpowershell scriptがLinux側から実行できなくなる現象が起きた。 結論としてはWindows 10 ビルド19042.782でのバグ(あるいはデフォルト設定の変更?)っぽいことが分かり、Windows Updateを適用したところ解決した。

この記事ではせっかくなのでWSL2 VMからのWindowsホストBinaryの実行について掘り下げたいと思う。

その際に調査した内容はあまり有益ではないので興味ある人だけ読んで欲しい。

調査

まず、問題のWSL2からWindows binaryが実行できない状況について以下の事実が確認できた。ただし実行ログなどは残っていないためあまり信用しないでほしい。

Build 19042.782 環境

  • powershell から wsl コマンドで bash を実行する際
    • /mnt/c/Users/MysticDoll 以下では powershell.exe が実行可能。
    • $HOME 上では powershell.exe を実行した際、以下の現象が起きた
      • ^C がttyから送信できない、またttyの応答が消える(仮想端末のキー入力が無視される)
  • dbus-daemonを起動しそこから起動したgnome-terminalをX転送しWindows上のX serverで利用している際
    • powershell.exe 等Windows側のバイナリを起動しようとした場合 ^C などが効かない(上記 $HOME での実行時と同じ状態)
      • この時 $PWD については関係なく同様の結果が得られた

また、Build 19042.789 においては上記は再現せず、問題なくWindows Binaryの実行が可能であった

WSL2からWindows Binaryを実行できる仕組み

docs.microsoft.com

こちらのドキュメントにWIndows/Linuxの相互運用性として纏められている、こちらの 相互運用性の無効化 の項目を見ると

ユーザーは、ルートとして次のコマンドを実行することで、1 つの WSL セッションに対して Windows ツールを実行する機能を無効にできます。

echo 0 > /proc/sys/fs/binfmt_misc/WSLInterop

Windows バイナリを再び有効にするには、すべての WSL セッションを終了して bash.exe を再実行するか、ルートとして次のコマンドを実行します。

echo 1 > /proc/sys/fs/binfmt_misc/WSLInterop

相互運用の無効化は、WSL セッション間で保持されません。新しいセッションが開始されると、相互運用は再び有効になります。

何やら /proc/sys/fs/binfmt_misc/WSLInterop という procfs のなにかで管理しているらしい。

binfmt_misc

binfmt_miscというのは好きなフォーマットのバイナリを好きなインタプリタで解釈して実行できるようにするためにLinuxに用意されたカーネルの機能らしい www.kernel.org

とりあえずWSL2を起動して当該のbinfmt_miscを見てみると以下の出力が得られる

mysticdoll@Himalayan:~$ cat /proc/sys/fs/binfmt_misc/WSLInterop 
enabled
interpreter /tools/init
flags: F
offset 0
magic 4d5a

とりあえず分かることは

  • 現在有効である
  • /tools/init というインタプリタで評価される
  • F フラグであるということ(他namespace/chroot環境下でも動くためにlazyにbinaryをロードせず、configuration timeにロードする? あまり分かっていない)
  • 対象となるbinaryは 0x4d5a から始まっている *2

ということである。

つまりWindows binaryを実行している本体は /tools/init であり、これがどういうものかというのが知りたいわけである。

/tools/init を探す

「探す」と言っている通り、WSL上で ls /tools/init して見つからないというわけである。よってなんらかの検索をする前に思いつく方法で探すだけ探してみようと思う。

find

古典的なfindをする、当然こんな方法で見つかるとは思っていないが一応やってみることにした。

結果を載せても文字数の無駄なので割愛します

適当なWindows binaryを実行し、 /proc/{pid}/exe を見てみる

とりあえず仮想端末の別のタブで PowerShellを起動してみる。するとそれっぽいのがいるのが分かる。

mysticdoll@Himalayan:~$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   1396   784 ?        Sl   Feb07   0:00 /init
mysticd+  1145  0.0  0.0  10000  5128 pts/2    Ss   16:57   0:00 bash
mysticd+  1156  0.0  0.0    804     4 pts/2    S+   16:57   0:00 /tools/init /mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe

とりあえず /tools/init を使ってるのは 1156 らしいので見てみる

mysticdoll@Himalayan:~$ ls -al /proc/1156/exe 
lrwxrwxrwx 1 mysticdoll mysticdoll 0 Feb  8 16:59 /proc/1156/exe -> /tools/init

どうやらリンク先ファイルはユーザランドからは見れない様子なので諦めるしかないっぽい。

stracebash経由から動作を見る

$ sudo strace -p {pid} -f

で対象となるbashをtraceし、powershell.exeを実行する。trace結果は以下。

[pid  1297] execve("/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe", ["powershell.exe"], 0x55b487b67ae0 /* 25 vars */) = 0
[pid  1297] arch_prctl(ARCH_SET_FS, 0x29c800) = 0
[pid  1297] set_tid_address(0x29c838)   = 1297
[pid  1297] brk(NULL)                   = 0xf55000
[pid  1297] brk(0xf56000)               = 0xf56000
[pid  1297] sched_getaffinity(0, 128, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) = 32
[pid  1297] getpid()                    = 1297
[pid  1297] getcwd("/home/mysticdoll", 4096) = 17
[pid  1297] uname({sysname="Linux", nodename="Himalayan", ...}) = 0
[pid  1297] getcwd("/home/mysticdoll", 4096) = 17
[pid  1297] open("/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_PATH) = 3
[pid  1297] readlink("/proc/self/fd/3", "/mnt/c/WINDOWS/System32/WindowsP"..., 4095) = 61
[pid  1297] fstat(3, {st_mode=S_IFREG|0555, st_size=452608, ...}) = 0
[pid  1297] stat("/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe", {st_mode=S_IFREG|0555, st_size=452608, ...}) = 0
[pid  1297] close(3)                    = 0
[pid  1297] open("/proc/self/mountinfo", O_RDONLY) = 3
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="33 24 8:16 / / rw,relatime - ext"..., iov_len=1024}], 2) = 1024
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="de=755\n46 45 0:28 / /sys/fs/cgro"..., iov_len=1024}], 2) = 1024
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="group cgroup rw,net_prio\n57 45 0"..., iov_len=1024}], 2) = 461
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="", iov_len=1024}], 2) = 0
[pid  1297] close(3)                    = 0
[pid  1297] getcwd("/home/mysticdoll", 4096) = 17
[pid  1297] open("/proc/self/mountinfo", O_RDONLY) = 3
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="33 24 8:16 / / rw,relatime - ext"..., iov_len=1024}], 2) = 1024
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="de=755\n46 45 0:28 / /sys/fs/cgro"..., iov_len=1024}], 2) = 1024
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="group cgroup rw,net_prio\n57 45 0"..., iov_len=1024}], 2) = 461
[pid  1297] readv(3, [{iov_base="", iov_len=0}, {iov_base="", iov_len=1024}], 2) = 0
[pid  1297] close(3)                    = 0
[pid  1297] ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
[pid  1297] ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
[pid  1297] ioctl(2, TCGETS, {B38400 opost isig icanon echo ...}) = 0
[pid  1297] ioctl(0, TIOCGPGRP, [1297]) = 0
[pid  1297] getpgid(0)                  = 1297
[pid  1297] fstat(0, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
[pid  1297] fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
[pid  1297] fstat(2, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0
[pid  1297] ioctl(0, TIOCGWINSZ, {ws_row=55, ws_col=166, ws_xpixel=0, ws_ypixel=0}) = 0
[pid  1297] ioctl(0, SNDCTL_TMR_START or TCSETS, {B38400 -opost -isig -icanon -echo ...}) = 0
[pid  1297] dup(0)                      = 3
[pid  1297] socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0) = 4
[pid  1297] bind(4, {sa_family=AF_VSOCK, sa_data="\0\0\377\377\377\377\377\377\377\377\0\0\0\0"}, 16) = 0
[pid  1297] getsockname(4, {sa_family=AF_VSOCK, sa_data="\0\0\322\22z\211\377\377\377\377\0\0\0\0"}, [16]) = 0
[pid  1297] listen(4, 4)                = 0
[pid  1297] socket(AF_UNIX, SOCK_SEQPACKET, 0) = 5
[pid  1297] connect(5, {sa_family=AF_UNIX, sun_path="/run/WSL/948_interop"}, 110) = 0
[pid  1297] write(5, "\6\0\0\0\356\0\0\0\322\22z\211\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 238) = 238
[pid  1297] accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0]p\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 6
[pid  1297] accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0^p\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 7
[pid  1297] accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0_p\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 8
[pid  1297] accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0`p\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 9
[pid  1297] close(4)                    = 0
[pid  1297] rt_sigprocmask(SIG_BLOCK, [INT WINCH], NULL, 8) = 0
[pid  1297] signalfd4(-1, [INT WINCH], 8, 0) = 4
[pid  1297] poll([{fd=0, events=POLLIN}, {fd=7, events=POLLIN}, {fd=8, events=POLLIN}, {fd=9, events=POLLIN}, {fd=4, events=POLLIN}], 5, -1) = 1 ([{fd=9, revents=POLL
IN}])
[pid  1297] recvfrom(9, "\t\0\0\0 \0\0\0", 8, MSG_WAITALL, NULL, NULL) = 8
[pid  1297] brk(0xf58000)               = 0xf58000
[pid  1297] recvfrom(9, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 24, 0, NULL, NULL) = 24
[pid  1297] poll([{fd=0, events=POLLIN}, {fd=7, events=POLLIN}, {fd=8, events=POLLIN}, {fd=9, events=POLLIN}, {fd=4, events=POLLIN}], 5, -1) = 1 ([{fd=7, revents=POLL
IN}])

このあたりがもの凄く怪しい

[pid  1297] connect(5, {sa_family=AF_UNIX, sun_path="/run/WSL/948_interop"}, 110) = 0

ここから推測するに、socketとして生えている /run/WSL/{id?}_interop を経由してプロセス生成関連のやり取りをしていそう。

このbashExploroer.exe を起動する場合以下のstrace出力が得られる。

[pid  1443] execve("/mnt/c/WINDOWS/Explorer.exe", ["Explorer.exe", "."], 0x55b487b67ae0 /* 25 vars */) = 0
(中略)
[pid  1443] socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0) = 4
[pid  1443] bind(4, {sa_family=AF_VSOCK, sa_data="\0\0\377\377\377\377\377\377\377\377\0\0\0\0"}, 16) = 0
[pid  1443] getsockname(4, {sa_family=AF_VSOCK, sa_data="\0\0\327\22z\211\377\377\377\377\0\0\0\0"}, [16]) = 0
[pid  1443] listen(4, 4)                = 0
[pid  1443] socket(AF_UNIX, SOCK_SEQPACKET, 0) = 5
[pid  1443] connect(5, {sa_family=AF_UNIX, sun_path="/run/WSL/948_interop"}, 110) = 0
[pid  1443] write(5, "\6\0\0\0\254\0\0\0\327\22z\211\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\09\0\0\0Q\0\0\0~\0\0\0\2\0\0\0u\0\0\0L\0000\1\1C:\\WINDOWS\\Explorer.exe\0\\\\wsl$\\Ubuntu-20.04\\home\\mysticdoll\0WSLENV=\0\0Explorer.exe\0.\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 172)
 = 172
(中略)
[pid  1443] exit_group(1)               = ?
[pid  1443] +++ exited with 1 +++

ここからも /run/WSL/948_interop がなにやらプロセスに対して引数他を渡していることが分かる。

また、PowerShellが出力した文字をLinux側に出力している部分を見ると

read(7, "Windows PowerShell\33[63X\r\nCopyright (C) Microsoft Corporation. All rights reserved.\33[24X\r\n\33[81X\r\n\346\226\260\343\201\227\343\201\204\343\202\257\343\203\255\343\202\271\343\203\227\343\203\251\343\203\203\343\203\210\343\203\225\343\202\251\343\203\274\343\203\240\343\20
1\256 PowerShell \343\202\222\343\201\212\350\251\246\343\201\227\343\201\217\343\201\240\343\201\225\343\201\204 https://aka.ms/pscore6", 4096) = 200

とあり、よって fd が7のsocketを探すと

socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0) = 4
(中略)
accept4(4, {sa_family=AF_VSOCK, sa_data="\0\0&q\351\360\2\0\0\0\0\0\0\0"}, [16], SOCK_CLOEXEC) = 7

となっているため、対話部分の実装については vsock(7) *3を立てることでホストマシン上のプロセスとやりとりしていることが分かった。

まとめ

これらから以下が推測できる

  • WSL2上からのプロセス実行は binfmt_misc によって実現されている
  • binfmt_misc のinterpretor /tools/init は以下の事をしている
    • /run/WSL/{id?}_interop を経由してホストマシンのWindowsプロセスの生成をする
    • vsock(7) を利用してホストマシン上のプロセスのttyをLinuxプロセスに転送している

細かいところは適当だけどひとまずこのあたりで終わりにします。