この記事は「富士通クラウドテクノロジーズ Advent Calendar 2023」の12日目の記事です。
前日は @ks2022 さんの「アラート対応の自動化」でした。
トラブル時にはどうしても情報整備まで気が回らないので、自動でストック情報にまとめる仕組みがあるのは便利ですね。属人化を防ぐのにも役立ちそうです。
TL; DR
データ整形のワンライナー、UIで使えたら便利では?
はじめに
手元にあるファイルから欲しいデータを掬いとる、一度限りのプログラミング。ワンライナーには儚いロマンが詰まっています。
...とはいえ、パイプを流れる中間データを想像するのは大変です。
cat users.json | jq -r .users[].name | awk '{print $1}' | sort | uniq -c > first_name_count.csv
工程ごとに都度出力しながら見ればイメージは付きますが、データが大きいとスクロールが流れて見づらいです。headで絞れば各行の内容は見やすくなりますが、今度は都度ワンライナーを書き換えるのが手間です。
ワンライナーのライブ感はそのままに、UI上でデータを見ながら変換ができたらいいのにな...
つくったもの
というわけで作りました。ブラウザ上で動きます。
まず、下のテキストエリアにデータを入れます。
次に、コマンド(今はプログラミング言語しかありません)を選び、ワンライナーを入れます。
適用すると、テキストエリアが出力結果に書き変わります。
後は別のワンライナーを適用し、気に入るまで変換し続けましょう(気に入らなかったら元に戻せます)。
将来的には head
, tail
, wc
, sort
あたりも入れたいです 1。
どさくさに紛れて「Pangaea」というコマンドが入っていますが、これは自作言語です。 抱き合わせは売名の常套手段
デザイン
Vite + React + TypeScriptで作成しています。
CSSは苦手ですが、Chakra UIで事なきを得ました。
ロゴには英字フォント「Line」を使用しています。直線的でおしゃれ
仕組み
UI上でのコマンドの実行
なるべく構成をシンプルにしたかった(デプロイの手間やコマンド実行によるセキュリティを考えるとバックエンドも持ちたくない)ので、WASIを使用しました。
WASIバイナリのダウンロードを除き、オフラインで動作が完結します。
フロントエンド上でのWASIコマンド実行にはRunnoを使用しています。
WASMなので実行環境がサンドボックス化されており、ファイル読み込みや通信等は無効化されています2。
Runnoでは、デフォルトで以下のコマンドを使用可能です。
python
ruby
-
quickjs
(JavaScript) sqlite
clang
clangpp
php-cgi
実行も1関数で完結します。
headlessRunCode("python", sourceCode, stdin).then(value => {
// 予期せぬクラッシュ
if (value.resultType !== "complete") {
console.log(`failed to run: ${JSON.stringify(value)}`)
return
}
// エラー発生
if (value.stderr !== "") {
alert(value.stderr)
return
}
// 正常終了
console.log(value.stdout)
})
自前のWASIコマンドも実行可能です。シグネチャが違うので、デフォルトのコマンドと共存させる場合は要注意です(型合わせの実装は こちら)。
const result = await WASI.start(fetch("/binary.wasm"), {
args: ["binary-name", "--do-something", "some-file.txt"],
stdout: (out) => console.log("stdout", out),
stderr: (err) => console.error("stderr", err),
stdin: () => prompt("stdin:"),
});
WASIバイナリ作成
続いて、自前で追加したいコマンドのWASIを用意します。ワンライナーといえばやはり jq
と awk
は欲しいですね。
ちょうどGo1.21からビルドターゲットにWASIが追加されたので、Go実装のjq, AWKをWASIにビルドして使用します。
GOOS=wasip1 GOARCH=wasm go build -o main.wasm
これで必要コマンドが揃いました。
ハマったところ
ここまで順風満帆かのように書いてきましたが、実際は色々な問題にハマり続けていました...
特にGitHub Pages絡みのトラブルシューティングは骨が折れました リンク先記事の先人の方々には感謝してもしきれません。
Chakra UIがレンダリングされない
Chakra UIのコンポーネントにCSSが反映されない問題。App
をChakraProvier
でラップする必要がありました
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ChakraProvider theme={theme}>
<App />
</ChakraProvider>
</React.StrictMode>,
)
Runnoで標準入力読み込みが無限ループする
WASI実行メソッド WASI.start
でハマった問題。標準入力を以下のように実装したところ標準入力読み込みが無限ループしてしまいました。
const result = await WASI.start(fetch(path), {
// ...
stdin: stdin,
})
こちらは仕様によるもので、stdin
を対話的に読み込めるよう stdin関数が null
を返すまで繰り返し呼び出されるという挙動でした。
正しくは以下のように実装します。クロージャは偉大3。
const result = await WASI.start(fetch(path), {
// ...
stdin: (() => {
// HACK: stdinを一度だけ返すようにクロージャを使用
let done = false
return () => {
if (done) {
return null
}
done = true
return stdin
}
})(),
})
Runnoで標準入力読み込みがハングする
@runno/runtime v0.6.2
で修正されたため現在は以下の対処は不要です!
PR即リリースいただけてありがたい~
ここからはあくまで試行錯誤の記録としてお読みください。
試行錯誤の記録(クリックで展開)
標準入力を末尾まで取得しようとするとハングしてしまいました。
# 標準入力を配列形式で全行取得
p readlines
結論としては、入力末尾に EOF
を追加していないのが原因でした。
Runnoとしては、対話的に標準入力を1行ずつ受け取り、終わったら ctrl + d
を押して終了する想定のようです。
This demo writes whatever you type to the file specified in args. When the program finishes you'll see a file in the filesystem with your contents.
To finish entering text press ctrl+d.
このデモでは、入力内容を引数に指定したファイルに書き込みます。プログラム終了時に、ファイルシステムに入力した内容のファイルが作成されます。テキスト入力を終了するにはctrl + dを押してください。
一方、今回使用した headlessRunCode
では標準入力を文字列の形で事前に渡しているため、ctrl + d
によりEOFを追記することができません。
実装を追っていくと、Runnoのランタイム内部に手を加えないと解消しないことが分かりました。
export async function headlessRunFS(
runtime: Runtime,
entryPath: string,
fs: WASIFS,
stdin?: string
): Promise<RunResult> {
// ...
if (stdin) {
workerHost.pushStdin(stdin);
// 以下の追加が必要!
workerHost.pushEOF();
}
// ...
}
Runnoのランタイム自体を変更する必要が出てきたので、その場しのぎですがソースコードにパッチを当てることにしました。幸い、npmにパッチを当てるツール patch-package があったので、手で書き換えた修正をパッチ化、 npm install
時に自動適用できるようにしました。
https://www.npmjs.com/package/patch-package
参考記事
https://bagelee.com/programming/javascript-2/patch-package/
できたパッチはこのような内容です。minifyされてたから読むの辛かった... (そもそもminifyされたコードを手で直してはいけません)
diff --git a/node_modules/@runno/runtime/dist/runtime.js b/node_modules/@runno/runtime/dist/runtime.js
index 813773f..5645bdf 100644
--- a/node_modules/@runno/runtime/dist/runtime.js
+++ b/node_modules/@runno/runtime/dist/runtime.js
@@ -31445,7 +31445,11 @@ async function ry(i, e, t, r) {
s.stderr += c, s.tty += c;
}
});
- r && l.pushStdin(r);
+ if (r) {
+ l.pushStdin(r);
+ console.log("HACK: add EOF to avoid hang");
+ l.pushEOF();
+ }
const O = await l.start();
return s.fs = { ...t, ...O.fs }, s.exitCode = O.exitCode, s;
}
後から振り返ると、この機能はPR作る前の動作確認としても便利ですね。
GitHub Pages作成時に、生成物をfeatureブランチへpushできない
GitHub Pagesデプロイにはactions-gh-pagesのGitHub Actionを使用しました。
このactionではビルド生成物featureブランチ gh-pages
にpushすることで管理していますが、このブランチへのpushが失敗してしまいました。
GITHUB_TOKEN
の権限が弱くなったのが原因だったため、明示的にpushの権限を与えました。
参考記事
GitHub pagesでNot Foundが発生する
デプロイが成功したと思ったら、今度はscript内のjsファイルやwasmファイル等が404で読み込めません。
これはパスがずれているのが原因でした。
パス | |
---|---|
エンドポイント | https://syuparn.github.io/linerduper/ |
表記 | /hoge.js |
期待するパス | https://syuparn.github.io/linerduper/hoge.js |
実際のパス | https://syuparn.github.io/hoge.js |
index.htmlでのJSファイル読み込み
GitHub Pagesにデプロイするときだけ defineConfig
の base
にパスを追加しました。
参考記事
WASMファイル
上記でも fetch
のパスは修正されないため、GitHub Pages上のフルパスを指定しました。
強引ですが、WASI自体を変更することはめったに無いので問題ない想定です。
SharedArrayBuffer
が未定義エラー
WASI実行時に以下のエラーが発生しました。
Uncaught ReferenceError: SharedArrayBuffer is not defined
これは意図的なもので、セキュリティ対策として SharedArrayBuffer
が動作できる条件を「同じオリジンからしかオブジェクトを参照できない状態」に限定しているためです。
具体的には以下のレスポンスヘッダが有効になっていないと使えません。
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
対策として、Service Worker を使用してレスポンスヘッダ追加しました。仕組みが複雑なので、詳細は以下の参考記事をご覧ください。
インターネット上の信頼できないwasmファイルや、利用者が指定したURLに対してこの方法を使うのは非推奨です。
ちなみに、Viteのミドルウェアでヘッダを追加する方法も試しましたが上手くいきませんでした。
GitHub Pagesはカスタムヘッダーに対応するまで無理なようです。
追加した実装
おわりに
以上、ブラウザ上で動くワンライナーエディタ(?)の紹介でした。構想2年、実現方法を考えあぐねていたのですが、Runnoとの出逢い、そしてGoのWASIサポートによってついに形になりました。
WASIはまだまだ色々なところに活用できそうです。後半はWASIよりもGitHub Pagesの仕様の話になってしまいましたが...
この記事は「富士通クラウドテクノロジーズ Advent Calendar 2023」の12日目の記事でした。
明日は、@kato-hiroki-783さんがredfishAPI or vsphere自動化 or homelabについて書いてくださるようです。効率的なインフラ管理のTipsが聞けそうですね。お楽しみに!