Note
この記事は whywaita Advent Calendar 2024 7 日目 の記事でしたが、遅刻しました。
前回は neotaso さんでした。はじめまして。
次回は 抹茶(matchaism) さんの記事です。はじめまして。僕が投稿遅刻したのでもう出ています。
whywaita コミュニティのみなさん、はじめまして。 whywaita と同じ会社に勤めている 1994 年生まれの @3846masa です。
whywaita とは同期で、大学時代にお互い異なる所属サークルでしたが、なんか一緒に LT イベントしたとかで知り合いました。
普段ブログ書かないので、あんまりいいネタが思いつかなかったんですが、お手柔らかによろしくお願いします。
whywaita コミュニティのみなさんは、おそらくプログラマの方が多いんじゃないかなと感じてます。
プログラマのみなさんにはわかってもらえると思う1んですが、Chromium が積まれた謎デバイス2向け Web アプリデバッグって面倒じゃないですか。
Chromium が積まれた謎デバイスは、「Chrome DevTools Protocol のポートは 9222 です」とか用意してくれてて、そこに手元の Chrome から chrome://inspect すれば DevTools が開けます。
でも、謎デバイスを作っている人たちは、なぜかわからないんですが、ポート番号みんな好き勝手決めるので、どれがどれだか覚えられないわけです。
これが普段の業務で「P 社の端末だから 12345 で、T 社の端末だから 54321 で...」ってやってると、病みます。
あと、単純にデバイスが多いとデバイスごとの IP アドレスとかも忘れます。ブロードキャスト通信するとクビになる呪いをかけられているので、mDNS とかも使えないし。
というわけで、複数の Chrome DevTools Protocol をまとめて、1 台の Chrome のようにみせかけたら、IP アドレスもポート番号も 1 つだけ覚えればよくなって、幸せですよねという話をします。
HTTP REST API と WebSockets API で組まれているプロトコルです。
あとは https://chromedevtools.github.io/devtools-protocol/ 読んでください。
ようは HTTP なので、適当に Node.js で HTTP サーバー立てて、CDP のリクエストが来たら適当に流せばよいです。
まずはブラウザになりすまします。
import fastify from 'fastify';
const app = fastify();
app.get('/json/version', (_, reply) => {
reply.send({
'Protocol-Version': '1.3',
Browser: 'Chrome/999.9.9.9',
});
});
app.get('/json/list', (_, reply) => {
reply.send([
{
id: '8f76cf89-8a41-4b4a-b42c-b15f1003c902',
type: 'page',
url: 'https://example.com/',
title: '存在しないタブ',
},
]);
});
void app.listen({ host: '0.0.0.0', port: 9222 });そして、Access local servers and Chrome instances with port forwarding | Chrome DevTools | Chrome for Developers の手順で Target discovery settings に登録します。ポートフォワーディングはしなくていいです。
すると、なんか出ます。
作戦としては、
- 複数デバイスのタブ情報から最初のタブだけ抽出して
/json/listをつくる- サイネージなので、複数タブ開いてることはない前提とする
/devtools/page/{id}を本物のデバイスにプロキシする
というわけで、適当にこんな感じのコードを書きます。
import fastify from 'fastify';
import proxy from '@fastify/http-proxy';
const app = fastify();
const DEVICES = [
{ name: 'Device 1', origin: 'http://device01.local:9222' },
{ name: 'Device 2', origin: 'http://device02.local:12345' },
{ name: 'Device 3', origin: 'http://device03.local:54321' },
];
const PAGE_ID_ORIGIN_MAP = new Map();
app.get('/json/list', async (_, reply) => {
const list = await Promise.all(
DEVICES.map((device) => {
return fetch(`${device.origin}/json/list`)
.then((r) => r.json())
.then((pages) => [device, pages[0]]);
}),
);
for (const [device, page] of list) {
PAGE_ID_ORIGIN_MAP.set(page.id, device.origin);
}
reply.send(
list.map(([d, p]) => ({ ...p, title: `${d.name} - ${p.title}` })),
);
});
app.register(proxy, {
prefix: '/devtools/page/:pageId',
rewritePrefix: '/devtools/page/:pageId',
upstream: void 0,
wsUpstream: void 0,
replyOptions: {
getUpstream(req) {
return PAGE_ID_ORIGIN_MAP.get(req.params.pageId);
},
},
websocket: true,
});
void app.listen({ host: '0.0.0.0', port: 9222 });/json/list にリクエストがきたときに、各デバイスの /json/list から先頭 1 つ目を集めて、Map に pageId とデバイスのエンドポイントをマッピングしておきます。
そして、@fastify/http-proxy を使って /devtools/page/:pageId にきた WebSocket を Map の情報をもとに転送します。
実際に試してみると、こんな感じになります。
https://i.imgur.com/yIhF5eG.mp4
いかがでしたか?みなさんの業務効率化に役立つと嬉しいですね。
