うなすけとあれこれ

2021年01月17日

ElectronでPocketリーダーアプリを作っています

開発中のアプリのスクリーンショット

どんなものを作っているのか

Pocketというサービスをご存知でしょうか。あとで読みたい記事を保存しておけるサービスです。僕はこのサービスのヘビーユーザー1で、Feedリーダーからほぼ毎日何かの記事を保存し、また保存しているリストから記事を読んでいます。

この、Pocketにストックしてある記事を手軽にサクサクと読んでいきたいと思い、手の配置を固定したままどんどんと記事を読んでいけるリーダーを作成しています。(右手で記事リストを移動、左手で記事ページをスクロール)

動機

このPocketですが、利用にあたってはiOSアプリ、Androidアプリ、macOSアプリ、公式Webアプリがあるものの、Windowsには公式のアプリが存在していません。FAQでも、"Under Consideration" のままずっとステータスが変化していません。またサードパーティー製のものも見付かりません。

Webアプリを使えば済む話ではありますが、Webでは記事を都度別タブで開いて読んだら閉じてアーカイブ、というのを繰り返す必要があり、だんだんと未読消化が億劫になってきます。

macアプリのようなものがWindowsにもあれば……というのが主な開発の動機です。

また、自分のスキルとして、JavaScriptでアプリケーションをイチから構築したことがなく、そのスキルを向上させたいという思い2もありました。

構成

electron-forgeから提供されている Webpack+TypeScript templateをベースに、React、Redux、Chakra UIを使用して開発しています。どれも仕事ではあまり触れたことがなく、探り探り実装を進めています。

ベータテスター募集中

開発中のアプリのベータテスターを募集します!「あとで読む」の管理などにPocketを使っている方をお待ちしています~~~~

Pocketリーダーアプリのベータテスター募集! | うなすけ @yu_suke1994
https://t.co/EHMUPSyXft #bosyu

— うなすけ (@yu_suke1994) January 9, 2021

現時点で、本当に基本的な機能の実装は完了しています。今年中の正式公開を目指して、不具合の修正、完成度の向上を行っている途中です。あと数人ほどベータテスターを募集していますので、気になる方はよろしくお願いします。


  1. 2017年にPocketから読んだ記事数が全ユーザーのtop 1%以内に入ったというメールが来たくらいには。 

  2. Railsを主戦場としている自分が今後学ぶべき技術について(随筆) 

Tweet
2021年01月17日
2020年12月13日

Railsを主戦場としている自分が今後学ぶべき技術について(随筆)

Rails の問題は Rails のベストプラクティスがフロントエンドのベストプラクティスの邪魔になるどころか全く逆方向で相反してる点です。DHHの思想がフロントエンドと根本的に逆行してる。そういう人が作るフレームワークなのでwebpackerの抽象化を根本的に間違ったりする。

— prev.js (@mizchi) December 1, 2020

昨日もリプライで少し書いたけど、DHH自体が直近のHeyの開発でも明確にJavaScriptというものを触れないようにすることを是としているような主張をしているので、DHH wayが色濃く反映される以上この状態はもう避けられない気がしている

— potato4d / Takuma HANATANI (@potato4d) December 1, 2020

Railsがフロントエンドの最先端をゆく人々1から良く思われないのは事実として。

View層においてJavaScriptのFrameworkもしくはコードの比重が増えていくとともに、バックエンドまで一気通貫で同じ言語を使用できればいいのにという思想からか、加えてRailsとモダンフロントエンドとの相性がよくないからか、Railsの出番は今後減っていくだろうという意見に対しての賛同が増えてきているのを感じている。

blitz-js prisma rails 倒し方 - zenn.dev/mizchi

実際のところ、HTMLがサーバーから返ってきて、ちょっとしたインタラクションがある、なんていうシンプルなWebアプリケーションの世界ならば、JavaScriptの存在は「塩」であれたかもしれない2。だが、例えば自分が関わったOOPartsだったり、Webブラウザ上でネイティブアプリケーションのようなリッチな体験を提供したいとなると、RailsはよくてJSONを返すAPI serverとしての立場でしか居られず、フロントでの資産をバックエンドでも使用したいという判断になった場合にはそもそもRubyの居場所が無くなってしまう。

そんなJavaScript優勢となりつつある昨今においてのRailsの優位点のひとつに、世界最強のORM3であるところのActiveRecordの存在が挙げられるが、それも前述の記事において触れられているPrismaの出来が良くなればひっくり返る。

RESTやActive Record patternとMVCの組み合わせ、CoCなどの先見性からRailsが優れていた時代はあるにせよ、それが永遠に続くとは限らない。仮にRailsが本当に使われなくなった時代が来たとして、別に自分はRailsと(言い方が悪いが)心中するつもりはないし、そんなつもりの人は世界に存在しないんじゃないか。そうならないように、みんなContributeしている。

では、Webの世界においてRailsが「レガシー」という世界になったとき、自分はどうすればいいだろうか。学生時代にRubyを触ったのが始まりとなって新卒でRailsを書き始め、そこからキャリアの大部分にRubyとRailsがしっかりと喰らい付いている自分は、Rubyに対してもRailsに対しても結構愛着がある。レガシーとなってほしくはない。採用され続ける選択肢であってほしい。

もちろん、Railsの仕事が消えることはない4。COBOLの仕事が消えないのと同様、既存のビジネスがRailsで回っているところは、余程のことがない限りはRailsを採用し続けるだろうし、他言語、他Frameworkに移行するとなってもRailsの経験は必要になる。何より、BasecampとHeyがそのビジネスを続けていく限りは、それに合わせてRails自体も進化し続けるだろうし、文字通りの「Railsの死」というものが来年のうちに来ることはないだろう5。しかし、では2年後は?3年後は?その先は?

Railsでは食べていけない、というのは何も仕事が無くなることのみを指しているのではなく、単価の面においても「上がらない」という状況も含む。昨今はプログラミングスクールがいくつも開設されている。その指導内容に玉石混淆はありつつも、良いスクールを卒業した方々はとても優秀で、自分の実力が追い越される日もそう遠くない。そうなったとき、市場原理からいっても、Railsが書ける、というスキルだけでは単価は伸び悩む。

自分の能力について、今後何について伸ばしていくべきなのかというのを考え直す必要がある6。今後需要が増えるスキルというのが何かを考えたとき、以下のものが挙げられるだろう。

ここから書くスキルについて、「そんなのもう主流な技術じゃないか、何周遅れの議論をしているんだお前は」という感想はぐうの音も出ないほど正しい。これは単純に、Railsでしか職を得てこなかった人間の切迫感を文章にしただけである。ここに挙げられている技術で手を動かすことができていなければ、今後は厳しいだろうという勝手な焦りであって、世の技術者全員がすべからくここに挙げた技術を使いこなせているべきだとは言っていない。

2018年の段階で私が知らないこと — Overreacted

まずはTypeScript。これは先程の文脈の通り。Webアプリケーションに関わる開発者なら、既に「書けないとまずい」という域になりつつあるだろう。では、具体的に「書けないとまずい」とはどのレベルを指して言うのだろう。自分は、既存のコードベースにおいて、軽いバグ修正や、Frameworkを使用したシンプルなWebサイト作成くらいはできる。しかし、それ以上のこと、例えばいくつかのライブラリを組み合せて0からWebアプリを構築するということが苦手だ、というかできない。TypeScriptという言語は特殊で、JavaScriptへと変換しなければ実行できないので、必然的にtscやそれら変換処理、bundleを担うwebpackなどのツール群、要はエコシステムへの深い理解も求められる。まあ、bundlingのことを考えるとTypeScriptがJavaScriptだろうが、あまり関係はない。エコシステムへの習熟だって、どんな言語においてもそうだろう。

次にGo。今やすっかり開発環境に欠かすことのできないDockerなどのコンテナ技術において、第一級市民言語であるところのGoは、実行ファイルが1バイナリに収まるdeployの手軽さ、M1 chipから始まるであろうArmプロセッサのシェア拡大という状況におけるクロスコンパイルの容易性、Rubyなどスクリプト言語と比較したときの実行速度からみても、API backendの実装言語としてさらにシェアが上昇していくことが予想できる。

自分にとってGoと似た立ち位置にRustが存在する。Rustに関しては、所有権などこれまでのプログラミングには出てこなかった概念が登場することから自分で勝手に壁をつくってしまっているところがあり、非常に良くない。

“Web” という領域で体験を追求していくなら、Service WorkerとWebAssemblyも見逃す訳にはいかない。WebAssemblyへコンパイルできる高級言語としても、Go/Rustはどちらかでも実戦で手を動かすことができるようになっていると助かるだろう。

言語に関係しないスキルとしては、設計がある。自分はClean Architectureを知らぬ。ActiveRecordに頼り、密結合したMVCばかり書いてきた。conventionに頼ってきたので、いざ自由に書くとなったときに指針が無く、途方に暮れることがままある。アプリケーション単体の設計についてもそうだし、大量のアクセス、大量のデータを捌くためのアーキテクチャというものについても知識が薄い。PofEAAを読み直す時期なのかもしれない。

そういう焦燥感があるので、PodcastのサイトをNext.jsで構築したりと、プライベートの時間はTypeScriptを書くことにしている。新しく触れる技術である程度の大きさのものを作ろうとすると、その技術について必要な知識のうち、自分に不足しているものがわかってくる。自分の場合、JavaScriptの場合はasync/awaitやPromiseが絡む、非同期処理が前提となった構造のコードが全然書けない。値を取得しようとしてはawaitを付け、しかしそこはasync functionではないというエラーが出る。こういうことを幾度となく繰り返している。Goの場合はgoroutineやchannel、contextについて全く無知である。存在は知っているが、どういうものか理解できていないし書けもしない。

フリーランスという立場上、技術についていけなくなり食い扶持を失うことを強く恐れている。別に社員時代にぬるま湯に漬かっていた訳ではなく、より一層その意識が強くなったというだけのこと。そんな中、自分が主戦場としている分野が、先駆者達からはいずれ避けるべきものになるという評価にあるということを知ると、反発したい気持ちになってしまうのは当然である。しかし、例えそれがポジショントークに過ぎず、新規事業においてはまだRailsが主選択肢たりえるとしても、一旦はその主張が正しいとしたときに自分はどういう方向に進むべきかというのを考えておきたい。これはその2020年末版。

余談 モチベーション

エンジニアを仕事にしてるみんな、なにをモチベーションにして、なんのために仕事や勉強してるの?

— ダンボー田中📦 (@ktanaka117) December 9, 2020

書いている途中にこのツイートが目に入り、そういえばこの記事だと食べていけるかどうかしか気にしていなくて目的が不明確だなと思ったので自分のモチベーションについても少し書く。

自分のモチベーションは「自分が作りたいものを作れるようになりたい」で、そのために技術の幅を広げたり、仕事や勉強をしているつもりだ。ある「作りたいもの」が出てきたとき、それが実際に可能かどうかの判断、逆にある技術を知ったときに「こういう便利なものが作れるんじゃないか」という思考ができるようにしておきたい。そして実際に作れるようになりたい。

プライベートの時間に書いているというTypeScriptも、自分の日々の困り事を解決するためのアプリケーションを作成するのにJavaSctiptが適していて、ならばとTypeScriptで書いているという経緯がある。 フリーランスになってから若干、「仕事にできるかどうか」が判断基準に加わったかもしれないが、原動力は変わっていないと思っている。もちろんお金は欲しいが。


  1. 発言を引用した2人に共通する点として、「抱えているユーザー数が多い」というのがあることに書きながら気づいた。 

  2. RailsDM 2019における講演より。 https://togetter.com/li/1330578 JavaScriptは塩のようなものであるという意見。 

  3. 誰かがそう言ってたような気がするんだけどソースが見つからなかった 

  4. Railsしか書けないプログラマーのために社内にRailsプロジェクトが用意されているという「Railsが福利厚生」という状況になったら嫌だな、と思った。 

  5. とは言ったものの、DHHの考えがモダンフントエンドと乖離したままRailsが成長するのであれば、年々その開きは大きくなっていくだろうが。 

  6. いつだって考え直すべきだが? 

Tweet
2020年12月13日
2020年12月12日

サーバーサイドエンジニアとして2020年に使った技術

使用したプログラミング言語統計

2020年のフロントエンドエンジニアの技術スタックの一例 | potato4d D(iary)

この記事と、TLで「これのバックエンド版が見たい」という発言に触発されたので書いてみます。口語体と文語体が入り乱れてるのは許してください。

冒頭のグラフはwakatimeで生成した今年1年間のプログラミング言語使用率です。2位はTypeScript、3位はTerraform、4位はYAMLでした。

立場

フリーランスで、主にRailsやAWSを使用しているサービスの運用、開発に関わっています。いくつもの会社を見てきた訳ではなく、数社に深く関わっている1都合上、視野が狭いかもしれません。

公開している成果としては クラウドゲーミング最新開発事例 - #CEDEC2020 - Speaker Deck があります。

長年RubyとRailsを書いてきたので、技術スタックがそのあたりに偏っています。

利用した技術一覧

太字は特記事項があるものです。また、挙げている技術は「ある程度触った」くらいで記載しており、全てに習熟している訳ではありません2

各項目について、上のほうがよく使っているものという傾向がありますが、厳密ではないです。

主軸としてはRuby/Rails/AWSの主要コンポーネント というところでした。

特記事項

Ruby

今年も僕の主戦場はRubyでした。あと数年はRuby以外を軸に仕事をしていくことになるんじゃないかな……?ただ2020年らしいことは全くしていなくて、例えばRuby 2.7で導入された機能、Pattern matchingやnumberd parameterは使った記憶がないです。(Pattern matchingは使おうとしたが、愚直に書いたほうが短かくなったので断念)

TypeScript

今年はJavaScriptをBrowser console以外で書いた記憶がありません。

Rails

キャリアがRailsから始まっているので、思い入れがあります。また、新しい現場でもすぐにコードを書けるところは助かっています。とは言えコードが肥大化してくると何がどうやって動いているのか把握するのには時間がかかりますが、これはまあどんなものでもそうでしょう。 業界においてRailsが優位にあるとすれば、その実装速度の早さと、コードやデータ量、ビジネスロジックがある程度成長しても耐えられるRubyの表現力及びActiveRecordの底力があるように思います。あと数年は新規実装としてのRailsの立場は残っていると思います。それ以降はまた状況が変わっていると思うのでわかりません。

React/Vue.js

これを1つにまとめたのは、主にRails appに組み込まれたものを書いていたためです。TypeScriptもそうなのですが、JavaScriptに関しては既存のコードベースに手を入れるということはできても、設計を含めた0から書き始めるということができていない、できないのが自分の弱みだと感じているので、プライベートの時間ではキャッチアップをしています。

Docker

コンテナ技術はもう前提条件と言えるくらいには利用が広がっていますね。ECSやKubernetesをはじめとしたエコシステムの発展もあり、開発環境から本番環境までDockerで環境が統一されていることの便利さ、docker-compose一発で開発環境が準備できる手軽さは一度体験するともう戻れないと言っていいです。

Docker for Macが遅い遅いと言われ続けている昨今ですが、僕の環境においては許容範囲というか、あまりそこで困らなかった印象があります。大分改善されているのではないでしょうか。それとも僕が鈍いのか。

Fargate/ECS

業務としては一切Kubernetesを使用しませんでした。関わっている範囲だとこの2つで十分要求を満たすことができていました。

IntelliJ Idea系

これは、今年春から JetBrains All Products Pack を契約して使い始めました。これについては1本ブログ記事を書くつもりで下書きを温めていたのですが、この際ですからそれから引用する形にします。

Rubyを第一言語としていますが、PythonやGoなどで書かれたAPI serverの開発を素早く完了させる必然性が増えました。 これまでは腰を据えてプロダクトの全容をあらかた把握した上で開発を進めることが多かったのですが、とにかく素早く問題点を発見して修正する、それもあまり習熟していない言語で、という状況に置かれることが増えてきました。 それに、習熟しているつもりのRubyですら、今までにない大規模なアプリケーションの機能開発においては全容を把握しておくということが困難です。

また、DB clientとしてのDataGripが非常に便利なのは嬉しい誤算でした。

おわりに

あまりRails界隈でこういう記事をみかけない気がするので、他の皆さんはどうなのか知りたいので書いてほしかったり……年の瀬に1年を振り返ってみるのは楽しかったです。


  1. おおっぴらに言わないだけで探せばわかります 

  2. インターネット・強・パーソンの皆さまは、あれがないこれがないこいつはショボいとお叩きになられるのでは……とビクビクしています。 

Tweet
2020年12月12日
2020年12月02日

ナ組Minecraftサーバーの監視について —マイクラサーバー監視2020—

はじめに

皆さん、Minecraftしてますか。サーバー、立ててますか。監視、してますか?

この記事では、2020年10月末に爆誕したMinecraftサーバー「ナ組サーバー」について、僕が勝手に監視している方法について現時点での構成をまとめておくものです。

なんか突然GCPを触りたくなったのでナ組マイクラ鯖を立てた

— 蜘蛛糸まな🕸️ / HolyGrail (@HolyGrail) October 30, 2020

注意

「マイクラサーバー監視2020」と題していますが、僕はこれまでにMinecraftのサーバーを運用した経験はありません。何ならここで言及するサーバーについても、構築したのは蜘蛛糸まな氏です。 単純に、今Minecraftサーバーの監視をするならどうするか、ということについて述べています。過去のベストプラクティスは知りません。

ナ組サーバーとは

ナナメさん (@7name_) とそのお友達が遊んでいるMinecraftサーバーです。構築は前述の通り、蜘蛛糸まな氏です。GCP上に構築されています。

#ナ組マイクラ - Twitter検索 / Twitter

この動画の冒頭でも成り立ちについては述べられています。

僕は、このサーバーへのアクセス権を頂いているので、そこから色々な作業を行い監視環境を構築してみました。以下は行なったことについてのまとめとなります。

監視ツールについて

まず、サーバーの監視を行うときに「何でモニタリングするか」というのが大きな部分を占めるでしょう。SaaSであればMackerelやDatadog、GCP上のサーバーであればCloud Monitoringを使っておくのもありでしょう。自分で構築するのであれば、PrometheusやZabbixやNagiosなどの選択肢もあります。

今回は、Grafanaとの連携で見た目が良いこと、新しめのものであることから、Prometheusで監視、Grafanaで可視化という構成を採用しています。

MinecraftへのPluginの導入

先ほどPrometheusを導入することに決めた、と書きましたが、そもそもMinecraftサーバーの状態をPrometheusから取得することができなければ無意味です。

Minecraftに導入できるPrometheus exporterはいつくか存在します。

star数、ドキュメントの量から、 sladkoff/minecraft-prometheus-exporter を導入することに決めました。このようなexporterが存在していたことも、Prometheus採用の一因です。

MinecraftサーバーへのPluginの導入については、各サーバーによってまちまちだと思うので詳しくは触れませんが、今回は特定のディレクトリ以下へplugin本体を展開しておくだけで済みました。

plugin自体の設定はYAMLで記述することができるので楽ですね。いい感じにやっておきましょう。

Prometheus, Grafana serverの構築

今回、MinecraftとPrometheus、及びGrafanaは同居させず、別のサーバーに分離することにしました。どちらもワンバイナリで動作するので、サーバーの適当なディレクトリに置いて起動させるだけで構築は大体完了です。追加でやることは、Prometheusのscraping_configsにMinecraftサーバーを追加で指定することと、Grafanaのdata sourceにPrometheusを追加すること程度です。

Webサーバー運用についてのHTTPS化などのエトセトラはここで詳細に記述することはしません。1

dashboardの作成

ここまででGrafanaを使用してダッシュボードを作成することができるようになっていはずなので、あとはチマチマと必要そうなグラフを並べたダッシュボードを作成します。ここで作成したダッシュボードの変化を見ているときが一番楽しいですね。

ダッシュボード

このダッシュボードについてですが、MinecraftサーバーのIPアドレスがわかってしまうため、一般に公開はしていません。2

アラートの設定

サーバーというものは、いつ止まってもおかしくありませんし、止まってしまっているときには何らかの方法で通知が欲しいものです。

Prometheusによるアラートといえば、Alertmanagerがあります。Alertmanagerは様々な手段でアラートを送信することができますが、一番手軽なものはWebhookによる通知なのではないでしょうか。そこで、Webhookによる通知をAlertmanagerから送信しようとしてみましたが、これがDiscordのWebhookには対応していませんでした。3

Alertmanager integration with Discord · Issue #1365 · prometheus/alertmanager

ナ組はコミュニケーションをDiscord上で行なっているので、そこに通知を集約できないのはつらいです。

しかし幸いなことに、Grafana自体のArert機能においては、DiscordのWebhookをサポートしていることが判明しました。

https://grafana.com/docs/grafana/latest/alerting/notifications/#list-of-supported-notifiers

そして、指定した閾値に達した場合に次のようにアラートをWebhook経由でDiscordに飛ばせることが確認できました。

テストアラートの様子

ここでは、Minecraftが使用しているJVMのThread数が異常な値になった場合に通知をするという単純なルールになっています。

まとめとアドベントカレンダーについて

このようにしてナ組のMinecraftサーバーは監視されています。Minecraftは10年近く遊ばれているゲームであり、インターネット上の情報も豊富ではあるものの古くなっている記述が沢山あります。監視に関する情報も、Prometheusを使用しているような比較的新しいものは見当らなかったので、こうやって記事にしてみました。

また、この記事は whywaita Advent Calendar 2020 の12/2を担当する記事となります。whywaitaさんは現在プライベートクラウドに関する仕事に関わっていたり、過去にPrometheus clusterを破壊するなどの輝かしい功績のあることですから、今回僕の行なった作業についても、より良い方法を知っているものと思います。一度、対面で教えていただきたいものですね。


  1. certbotを使用しています。それ以外の詳細については面倒になったので書きません。 

  2. わからないようにできないか?とがんばってみましたが僕には無理でした。 

  3. 代替手段として https://github.com/benjojo/alertmanager-discord がありますが、コンポーネントを増やすことになるので導入はしませんでした。 

Tweet
2020年12月02日
2020年11月30日

このブログをS3 Static website hosting + CloudFrontからAWS Amplifyに移行しました

amplify console

2017年からS3 Static website hostingとCloudFrontの組み合わせで運用してきたこのブログを、AWS Amplifyによる運用へと変更しました。

理由

S3 syncが遅い

記事、画像が増えてくると、S3 syncに時間がかかるようになってきます。大体10分前後かかるということで、ちょっとなんとかしたいなと思っていました。また、deployに使用している middleman-s3_sync gem の開発が停滞しているように見えるのも脱S3 syncの要因のひとつです。良く言えば安定している、と言えなくもないし、嫌ならS3 syncによるdeployをやめればいいという話ではあるかもしれませんが……

管理が楽

大した管理をしていたわけでもないですが、S3とCloudFrontの2つのサービスによる構成から、Amplifyだけを見れば良くなるので少し楽になります。

裏側の構成は変化しない

Amplifyも裏はCloudFront+S3っぽいし、何度か計測してると逆転することもあったので有意差はなさそう https://t.co/wa6hgje5PH

— うなすけ (@yu_suke1994) July 24, 2020

という感じで、Amplify consoleでもCloudFrontのdistrinution URLがCNAMEとして指定する値と出てくるように、CloudFrontがリクエストを受けるという構成は変化しないことがわかっていました。なので大した影響はないだろうと判断しました。

ハマり

ダウンタイムが発生する

元々のCloudFrontで握っているTLS証明書との兼ね合いか、一度既存のCloudFrontのdistributionを削除(無効化ではだめだった)してからでないとAmplify console側で以前のものと同じドメインを設定させることができないようです。

Switching Cloudfront+S3 => Amplify with custom domain requires downtime · Issue #22 · aws-amplify/amplify-console

そのためにダウンタイムは必ず発生してしまいます。

それがどのくらいかかるかについてですが、ダウンタイムは体感では5分もなかったように思います。

(特に予告なくシレッと移行作業を行いました)

Tweet
2020年11月30日
2020年10月17日

Kaigi on Rails STAY HOME Edition 配信の裏側

配信構成

御礼

Kaigi on Railsという初めてのイベントに参加していただいた皆様、素晴らしい発表をしてくださった発表者の皆様、惜しくも採択とはいかなかったものの、Proposalを提出してくださった皆様、協賛していただいたスポンサーの皆様、本当にありがとうございます。

Kaigi on RailsではProposal提出まわりと、yrindaさんと一緒に配信・動画まわりを担当していたうなすけです。

https://kaigionrails.org/team/

この記事では、完全オンライン開催にあたっての配信まわりについて、裏側がどうだったかについて触れるものです。リアル開催からオンライン開催に切り替えた苦労話なんかはチーフオーガナイザー氏が書いてくれたのでここでは触れません。

Kaigi on Railsのチーフオーガナイザーを務めました - okuramasafumiのブログ

はじめに本番構成

全てをすっ飛ばして、本番の配信環境をまず紹介します。

本番配信はメインのyrinda houseとサブのunasuke houseの2系統を用意して冗長化していました。メインで何か問題が発生したらサブ側の配信URLを広報し、そちらを視聴してもらうイメージでした。そのために2人が同じ内容をメインとサブで配信する必要があり、シーンの切り替えタイミング等は当日全員が入っているDiscordでタイミングを合わせて行っていました。

ライブ発表の場合の構成は、アプリケーション側のZoomを発表者の映像用に、モニターを1枚占有した状態のブラウザからZoomに入り発表資料用に、そしてこれらをOBSに流しこみました。 その他に、それらの画面に重ねる発表情報の枠、Zoomからの音声出力キャプチャなどの設定をライブ発表用に用意しました。

配信構成図

動画発表の場合は、発表する方々から事前に提出いただいた動画を、同様の見た目になるよう発表用の枠をかぶせて書き出し、当日は再生するのみの運用になるようにしました。

当日のタイムテーブルの進行については、発表者、休憩それぞれでOBSのシーンを作成し、基本的にはそれを上から切り替えていくという省エネ運用になるように配信担当側で協力して作り込んでいきました。

OBS scene構成

それまでのストーリー

まず当初、Kaigi on Railsはリアル会場で開催するつもりでした。ところが時世が変化し、どうしてもリアルで集まっての開催ができなくなり、他の多くのイベントと同様にインターネット上での配信による開催に切り替えることとなりました。

オンライン開催となったとき、ではどのような形式で開催するのかということが問題になってきます。配信プラットフォームはどれにするのか、どのように配信を行うのか、日付、時間帯、各トークの長さ、配信を委託するのか自分達でやるのか……

何度か話し合いを行った結果、運営チームにある程度の設備があることから、配信は自分達で行うことに、プラットフォームは強い決め手がある訳ではないですが、YouTube Liveを選択しました。議事録を振り返っていたのですが、全部録画もしくは配信ではなく、録画と配信を希望制にした理由は残っていませんでした。議論するまでもなく、そう決まったのだと思います。

オンラインイベントにすると決まった訳ですが、我々には知見が全くありませんでした。そこで、Kaigi on Railsそのものの宣伝も兼ね、「Kaigi on Rails new」というイベントを開催し、そこでオンラインイベントの “感じ” を掴むことにしました。

Kaigi on Rails new - Kaigi on Rails | Doorkeeper

結果様々なKPTを洗い出すことができ、本番までに何を準備すればよいか、どういう運用にすればいいのかが見えてきました。

その後は提出してもらった動画を編集し、書き出したものを共有したりで動画の準備は進んでいきます。機材がない方のために、事前に録画会をやろうという企画もありましたが、希望者少数のため実現しませんでした。

配信勢の皆さんに対しては、直前に配信テスト会を開催してZoomでの登壇の感じをつかんでもらう一方、配信スタッフ側でも実際にどういうオペレーションになるかの細部を詰めました。具体的には登壇者交代がどうなるか、などです。

そして、前述のような構成になりました。

当日起こったトラブル

反省点

音量のバラつき

事前に頂いていた動画の音量は正規化していたのですが、配信側の音量が想定以上に大きく、相対的に動画のほうが小さく聞こえてしまう問題がありました。アーカイブ公開版では再度正規化を行って、できる限りバラつきがないようにしてあります。

そもそもの配信構成について

振り返ってみると、配信にあたって回線を冗長化するよりは、オペレーターを冗長構成にすべきだったかもしれません。要するに配信環境を1箇所に構築し、そこに配信担当者が集合するという体制です。今回は何事もなく終わりましたし、何事があってもイベントが進行するように準備をしましたが、次回どうするかはまた考える必要があります。何事もそうですが。

参考にした情報

今後

Kaigi on Railsは来年も開催することが決定となっていますが、「やる」以外は何も決まっていません(本当に、何も……)。配信環境についても、今回の反省を活かして何かしらの改善を行っていきたいですね。例えばクラウド上からの配信にしたり、配信用機材を導入したりといったことが考えられます。1年かけて、ゆっくり考えていきたいです。


この記事はHHKB Professional HYBRID Type-SとHelixPicoとChoco60で書かれました。

Tweet
2020年10月17日
2020年09月26日

ItamaeのCIを travis-ci.org から travis-ci.com に移行しました

読む前に

タイトルに書いてあるように、travisの org から com に移行した、という話なのですが、そこに至るまでに様々な苦労をしたのでお願いだから最後まで読んでねぎらってください。

そもそもの始まり

ItamaeのCIには、travis-ci.org を使用していました。

CIというものは、たまに落ちることがあります。その原因は内部のコードが悪い場合や外部要因である場合などが挙げられます。ItamaeのCIも、例によってたまに落ちることがありました。

落ちているログ

たまに落ちることはよくて、落ちたCIを再度実行して通れば良いのです。しかし問題は、「落ちたtestを再度実行することができない」 というところにありました。

いつからなのかは不明ですが、travis-ci.org では失敗したtestを再実行するUIが消えてしまっており、空commitを積むなどしなければ同じコードでのtestの再実行ができなくなっていました。

再実行ができない様子

この “More Options” 内の “Requests” は一見再buildのリクエストのように見えますがそのような挙動はしません。

これは非常につらい。ので、CIをtravis-ci.orgではない別の何かに乗り換えることにしました。

Itamaeのテストについて

CIについて話す前に、Itamaeのtestにおける対象の組み合わせについて触れておきます。CIでは以下のような組み合わせに対してテストを行っています。

unitなのかintegrationなのか、というのは、unitはItamae gemの実装に対してテストを行い、integrationは用意されているrecipeをDocker containerに適用して意図した状態にできているかを確認するテストとなっています。

(以前はDigitalOceanで起動したインスタンスに対してrecipeの適用を行っていました)

選択肢

さて、OSSなら無料で使用できるCIサービスというものはいくつかありますが、代表的なものに以下の2つが上げられると思います。

新しもの好きということもあり、まずGitHub Actionsを試してみることにしました。

GitHub Actions

まずGitHub Actionsでのunit testは以下のようなYAMLで実行できます。

name: "unit test on ubuntu"

on:
  push:
    branches: "*"
jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-16.04, ubuntu-18.04]
        ruby: [2.2, 2.3, 2.4, 2.5, 2.6, 2.7, head]
      fail-fast: false
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - run: bundle install
      - run: bundle exec rake spec:unit

簡単ですね。これで問題なくunit testはpassするようになります。

GitHub Actionsでunit testが成功する様子

しかし、GitHub Actionsでは integration testがどうしても成功しないという問題に直面してしまいました。

GitHub Actionsでintegration testがランダムに落ちる様子

なぜintegration testが通らないのか

なぜ通らないのかの原因がわかればいいのですが、これが全くの不明でした。上記画像にあるように落ちているものもあれば通っているものもあり、全体的に不安定になっています。落ちている原因そのものはtmpディレクトリに書き込みができなくなって落ちているのですが、なぜそうなっているのかはわかりません。

友人に相談したところ、興味深い事実が明らかになりました。

Slack log1 Slack log2

「straceをonにするとpassする」 のです。

straceは何をするツールなのかと言うと、システムコールをトレースするツールです。それを有効にしただけで通るようになる、つまり実行に対してオーバーヘッドがかかるとtmpへの書き込みが成功してtestが成功する、という現象が発生しているようなのです。

なぜこのような状況になっているのかの調査はちょっとハードルが高すぎるので、GitHub Actionsの採用は見送ることにしました。

(self-hosted runnerを使用することでGitHub Actionsで使用されているインスタンスに特有の問題なのかを調査することもできますが、そこまでの元気は出ませんでした)

CircleCI

次に手を出したのはCircleCIです。CircleCIでは最近matrix jobを記述できるようになり、複数の条件を組み合わせたテストが書けるようになりました。

https://circleci.com/blog/circleci-matrix-jobs

とはいえ複雑なYAMLになってしまいました。

.circleci/config.yml を見る

version: 2.1
orbs:
  ruby: circleci/ruby@1.0.7

executors:
  docker:
    docker:
      - image: cimg/base:stable
  docker-1804:
    docker:
      - image: cimg/base:stable-18.04
  machine:
    machine:
      image: circleci/classic:201808-01

jobs:
  unit:
    parameters:
      ruby-version:
        type: string
      exec:
        type: executor
        default: ""
    executor: << parameters.exec >>
    steps:
      - checkout
      - ruby/install:
          version: << parameters.ruby-version >>
      - run: gem install bundler --version 1.17.3 --force
      - run: bundle install -j4
      - run: ruby -v
      - run: bundle exec rake spec:unit
  integration:
    parameters:
      ruby-version:
        type: string
    executor: machine
    # executor: docker
    steps:
      - checkout
      - setup_remote_docker:
          version: 18.06.0-ce
      - ruby/install:
          version: << parameters.ruby-version >>
      - run: gem install bundler --version 1.17.3 --force
      - run: bundle install -j4
      - run:
          command: |
            ruby -v
            export PATH=$HOME/.rvm/bin:$PATH
            ruby -v
      - run: bundle exec rake spec:integration:all
  unit-jit:
    parameters:
      ruby-version:
        type: string
    executor: docker
    steps:
      - checkout
      - ruby/install:
          version: << parameters.ruby-version >>
      - run: gem install bundler --version 1.17.3 --force
      - run: bundle install -j4
      - run: ruby -v
      - run: RUBYOPT=--jit bundle exec rake spec:unit
  integration-jit:
    parameters:
      ruby-version:
        type: string
    # executor: machine
    executor: docker
    steps:
      - checkout
      - setup_remote_docker:
          version: 18.06.0-ce
      - ruby/install:
          version: << parameters.ruby-version >>
      - run: gem install bundler --version 1.17.3 --force
      - run: bundle install -j4
      - run: ruby -v
      - run: RUBYOPT=--jit bundle exec rake spec:integration:all

workflows:
  version: 2
  all-test:
    jobs:
      - unit:
          exec:
            name: docker-1804
          matrix:
            parameters:
              ruby-version: ["2.3"]
      - unit:
          exec:
            name: docker
          matrix:
            parameters:
              ruby-version: ["2.4", "2.5", "2.6", "2.7"]
      - integration:
          matrix:
            parameters:
              ruby-version: ["2.3", "2.4", "2.5", "2.6", "2.7"]
  # all-test-with-jit:
  #   jobs:
      - unit-jit:
          matrix:
            parameters:
              ruby-version: ["2.6", "2.7"]
      - integration-jit:
          matrix:
            parameters:
              ruby-version: ["2.6", "2.7"]

CircleCIでItamaeのtestを実行するにあたり、いくつかの壁に突き当たったので紹介します。

ruby/install-depsの挙動

CircleCIにはOrbという仕組みがあり、汎用的な手順ならYAMLに記述しなくてもOrbを導入することによって記述を省略できる仕組みがあります。

Orb の概要 - CircleCI

Rubyの実行環境を用意するOrbとして、CircleCIが公式で用意しているのが circleci/ruby です。

このOrbですが、bundlerをインストールするのにGemfile.lockの存在をアテにしている部分があります。

https://github.com/CircleCI-Public/ruby-orb/blob/5fee9e2ae8fc7a88cce9ce4d9da4c562ead614b1/src/commands/install-deps.yml#L34-L41

Itamaeはgemであり、Gemfile.lockをリポジトリに含んではいません。ではこのOrbは使えないのかというとそうではなく、よく読むとわかるように bundler-version を渡すとGemfile.lockがなくても指定したversionのbundlerをインストールできるように見えます。 しかし既存のbundlerを上書いてしまってよいかの確認ダイアログが出るため、どちらにしろ上手く動きません。

bundlerを上書きできない

結局、bundlerのinstallは手で --force を付与したコマンドを実行させることで回避しました。

machine executorでRubyのversionが設定できない?

unit testはこれでうまくいくようになりましたが、dockerコマンドを使用する都合上、machine executorで実行しているintegration testでエラーが出るようになりました。それもJITを有効にしている場合のみ失敗します。

--jitが無効

エラーを見ると、--jit というオプションが不正というものでした。しかし、このスクリーンショットで使用しているRubyは2.6であり、--jit は有効なはずです。

不審に思い、Rubyのversionも出力するようにしたのが以下のスクリーンショットです。

Rubyのversionが一致しない

なんと、Ruby 2.6 をインストールしたはずなのに、使用されているのはRuby 2.3になっています。これはRVMのPATHをこねくりまわしてもどうしても解決することができませんでした。どうしてこんなことになるのでしょう。

setup_remote_dockerではvolume mountが使用できない

二進も三進もいかないので、remote dockerを使用してみることにしました。これは、unit testで使用しているDocker executorであればRubyのversionが正しく設定できているので、その環境においてdockerコマンドを使用できるようにするための仕組みです。

デプロイする Docker イメージを作成するには、セキュリティのために各ビルドに独立した環境を作成する特別な setup_remote_docker キーを使用する必要があります。 この環境はリモートで、完全に隔離され、Docker コマンドを実行するように構成されています。 ジョブで docker または docker-compose のコマンドが必要な場合は、.circleci/config.yml に setup_remote_docker ステップを追加します。

Docker コマンドの実行手順 - CircleCI

しかし、やはりエラーになってしまいます。これはテストの過程においてItamaeのコードをまるっとdocker container側にvolume mountしている部分があるのですが、remote dockerがvolume mountをサポートしていないためにエラーになります。

ジョブ空間からリモート Docker 内のコンテナにボリュームをマウントすること (およびその逆) はできません。 https://circleci.com/docs/ja/2.0/building-docker-images/#section=configuration

(docker cpはできますが)

という様々な躓きがあり、CircleCIを使うのはあきらめようと考えました。 (あきらめるまでに60回以上CIを回しています)

travis-ci は org から com へ移行できる

ではどこのCIを使おうか、となるのですが、そもそもtravis-ci.comへの統合が進められていることに気付きました。

2018年に、Travis CIはGitHub Appsとして導入できるようになり、OSSも travis-ci.com でCIを実行できるようになっています。

2020年の今でも travis-ci.org を使用しているので、試しに travis-ci.com に移行してみることにしました。

この移行はとても簡単で、 travis-ci.com にGitHub accountでログインしてMigrateボタンをクリックするだけです。

結果ですが、このように “Restart build” ボタンが出現しており、テストの再実行ができるようになりました!

Restartができる様子

まとめ

という諸々があり、ItamaeのCIは travis-ci.com に移行することになります。

これについて、第53回情報科学若手の会のLT枠で発表した資料を貼っておきます。

wakate2020
Tweet
2020年09月26日
2020年08月17日

UTF-8 の文字列をできる限り Shift_JIS に変換したい(実践編)

unicode nomalize by uconv

先日、きりきりやままさんがこのような記事を公開していました

UTF-8 の文字列をできる限り Shift_JIS に変換したい - きりきりやま

それでは実際にそのような文字列変換を行うにはどうすればよいのか、またコメントでiconvについて触れられていたので、この記事ではUnicodeにおけるNFKC正規化をどうやって行うのか試してみることにしました。

追記

Ruby

僕にとって文字列処理といえばRubyなので、まずは以下のようなscriptを書いてみました。

puts "\u304c"
puts "String#encode('Shift_JIS') => #{"\u304c".encode('Shift_JIS').inspect}"
puts "codepoints => #{"\u304c".codepoints}"
puts "NFKC normalized codepoints => #{"\u304c".unicode_normalize(:nfkc).codepoints}"
puts "Encode to SJIS with NFKC => #{"\u304c".unicode_normalize(:nfkc).encode('Shift_JIS').inspect}"

puts "=" * 20

puts "\u304b\u3099"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u304b\u3099".encode('Shift_JIS', undef: :replace).inspect}"
puts "codepoints => #{"\u304b\u3099".codepoints.inspect}"
puts "NFKC normalized codepoints => #{"\u304b\u3099".unicode_normalize(:nfkc).codepoints}"
puts "Encode to SJIS with NFKC => #{"\u304b\u3099".unicode_normalize(:nfkc).encode('Shift_JIS').inspect}"

puts "=" * 20

puts "\u0063\u006d"
puts "String#encode('Shift_JIS') => #{"\u0063\u006d".encode('Shift_JIS').inspect}"
puts "codepoints => #{"\u0063\u006d".codepoints}"
puts "NFKC normalized codepoints => #{"\u0063\u006d".unicode_normalize(:nfkc).codepoints}"

puts "=" * 20

puts "\u339d"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u339d".encode('Shift_JIS', undef: :replace).inspect}"
puts "codepoints => #{"\u339d".codepoints}"
puts "NFKC normalized codepoints => #{"\u339d".unicode_normalize(:nfkc).codepoints}"
$ ruby script.rb
が
String#encode('Shift_JIS') => "\x{82AA}"
codepoints => [12364]
NFKC normalized codepoints => [12364]
Encode to SJIS with NFKC => "\x{82AA}"
====================
が
String#encode('Shift_JIS', undef: :replace) => "\x{82A9}?"
codepoints => [12363, 12441]
NFKC normalized codepoints => [12364]
Encode to SJIS with NFKC => "\x{82AA}"
====================
cm
String#encode('Shift_JIS') => "cm"
codepoints => [99, 109]
NFKC normalized codepoints => [99, 109]
====================
㎝
String#encode('Shift_JIS', undef: :replace) => "?"
codepoints => [13213]
NFKC normalized codepoints => [99, 109]

https://wandbox.org/permlink/CQaSM6ffOHc0zLu6

Rubyにおいては、Unicode正規化を行うには String#unicode_normalize によって行うことができます。その際にoptionとして正規化の形式を指定することができます。とても簡単ですね。

https://docs.ruby-lang.org/ja/latest/class/String.html#I_UNICODE_NORMALIZE

Python

import unicodedata

print('\u304c (U+304c)')
print('codepoints => ', end='')
for char in '\u304c'.strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u304c').strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('=' * 20)

print('\u304b\u3099 (U+304b U+3099)')
print('codepoints => ', end='')
for char in '\u304b\u3099'.strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u304b\u3099').strip():
  print(hex(ord(char)), end='')
print()
print('=' * 20)

print('\u0063\u006d (U+0063 U+006d)')
print('codepoints => ', end='')
for char in '\u0063\u006d'.strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u0063\u006d').strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('=' * 20)

print('\u339d (U+339d)')
print('codepoints => ', end='')
for char in '\u339d'.strip():
  print(hex(ord(char)) + ' ' , end='')
print()
print('NFKC normalized codepoints => ', end='')
for char in unicodedata.normalize('NFKC', '\u339d').strip():
  print(hex(ord(char)) + ' ' , end='')
print()

https://wandbox.org/permlink/cMc7S5blWLZLLObD

Pythonにおいては、unicodedata モジュールをインポートすることによって使用できる unicodedata.normalize により、形式を指定して正規化を行うことができます。

unicodedata — Unicode データベース — Python 3.8.5 ドキュメント

Go

package main

import (
    "fmt"

    "strings"

    "unicode/utf8"

    "golang.org/x/text/unicode/norm"
)

func printCodepoints(str string) {
    fmt.Print("codepoints => ")
    for i, w := 0, 0; i < len(str); i += w {
        runeValue, width := utf8.DecodeRuneInString(str[i:])
        fmt.Printf("%U ", runeValue)
        w = width
    }
    fmt.Print("\n")
}

func main() {
    fmt.Println("\u304c (U+304c)")
    printCodepoints("\u304c")

    fmt.Print("NFKC normalized ")
    printCodepoints(norm.NFKC.String("\u304c"))

    fmt.Println(strings.Repeat("=", 20))

    fmt.Println("\u304b\u3099 (U+204c u+3099)")
    printCodepoints("\u304b\u3099")

    fmt.Print("NFKC normalized ")
    printCodepoints(norm.NFKC.String("\u304b\u3099"))

    fmt.Println(strings.Repeat("=", 20))

    fmt.Println("\u0063\u006d (U+0063 U+006d)")
    printCodepoints("\u0063\u006d")

    fmt.Print("NFKC normalized ")
    printCodepoints(norm.NFKC.String("\u0063\u006d"))

    fmt.Println(strings.Repeat("=", 20))

    fmt.Println("\u339d (U+339d)")
    printCodepoints("\u339d")

    fmt.Print("NFKC normalized ")
    printCodepoints(norm.NFKC.String("\u339d"))
}

https://play.golang.org/p/xG255G32mlJ

Goでは、norm packageを使用することで正規化を行うことができます。

JavaScript

// function from https://jsprimer.net/basic/string-unicode/#code-point-is-not-code-unit
function convertCodeUnits(str) {
    const codeUnits = [];
    for (let i = 0; i < str.length; i++) {
        codeUnits.push(str.charCodeAt(i).toString(16));
    }
    return codeUnits;
}


console.log('\u304c (U+304c)')
console.log('codepoints => ' + convertCodeUnits('\u304c'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u304c'.normalize('NFKC')))
console.log('=' .repeat(20))


console.log('\u304b\u3099 (U+304b U+3099)')
console.log('codepoints => ' + convertCodeUnits('\u304b\u3099'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u304b\u3099'.normalize('NFKC')))
console.log('='.repeat(20))

console.log('\u0063\u006d (U+0063 U+006d)')
console.log('codepoints => ' + convertCodeUnits('\u0063\u006d'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u0063\u006d'.normalize('NFKC')))
console.log('='.repeat(20))


console.log('\u339d (U+339d)')
console.log('codepoints => ' + convertCodeUnits('\u339d'))
console.log('NFKC normalized codepoints => ' + convertCodeUnits('\u339d'.normalize('NFKC')))
console.log('='.repeat(20))

https://wandbox.org/permlink/JLQH8LasdQo9ewgS

JavaScriptでは、 String.prototype.normalize() によって正規化を行うことができます。

それでは他のツールはどうでしょうか。

nkf

nkfはNetwork Kanji Filterの略で、古くからある文字コード変換ツールです。

https://ja.osdn.net/projects/nkf/

Rubyはnkfを同梱しているので、手軽に試すことができます。今回は一度Shift_JISに変換してからUTF-8に戻すことで、正しく変換できているかを確認してみます。

require 'kconv' # kconvはnkfのラッパーです

puts "\u304c (U+304c)"
puts "Endoce to SJIS by nkf => #{"\u304c".tosjis.inspect}"
puts "=" * 20

puts "\u304b\u3099 (U+304b U+3099)"
puts "Endoce to SJIS to UTF-8 by nkf => #{"\u304b\u3099".tosjis.toutf8}"

puts "=" * 20

puts "\u0063\u006d (U+0063 U+006d)"
puts "Endoce to SJIS by nkf => #{"\u0063\u006d".tosjis.inspect}"

puts "=" * 20

puts "\u339d (U+339d)"
puts "Endoce to SJIS to UTF-8 by nkf => #{"\u339d".tosjis.toutf8}"
$ ruby script.rb
が (U+304c)
Endoce to SJIS by nkf => "\x{82AA}"
====================
が (U+304b U+3099)
Endoce to SJIS to UTF-8 by nkf => 縺九y
====================
cm (U+0063 U+006d)
Endoce to SJIS by nkf => "cm"
====================
㎝ (U+339d)
Endoce to SJIS to UTF-8 by nkf => ㎝

このように、「が」(U+304B U+3099) の変換に失敗していることがわかります。そもそもnkfはUnicodeにおける正規化形式を指定できるのでしょうか。

nkfは2006-03-27にリリースされた 2.0.6 以降 (正確には2.0.6-beta2以降) においてUnicodeの正規化に対応するようになりましたが、「UTF8-MACの範囲のみ」と明言されています。

ここでの UTF-8-MAC は、macOSがAPFS以前1に採用していた HFS+ というファイルシステムにおいて使用されている正規化形式の通称2で、一見NFD形式のようで互換性のない正規化3を行っています。

nkfは入力においてのみUTF-8-MACを受け付けるようになっているようで、他の正規化形式に対応していません。

nkfにオプションから文字コードを指定した変換をして確かめてみましょう。

# nkf.rbとして保存
require 'nkf'

puts "\u304b\u3099 : U+304b U+3099"
puts "nkf --ic=UTF-8 --oc=Shift_JIS"
ga_to_sjis_from_utf8 = NKF.nkf('--ic=UTF-8 --oc=Shift_JIS', "\u304b\u3099")
puts ga_to_sjis_from_utf8.inspect
puts ga_to_sjis_from_utf8.encode('UTF-8')

puts "=" * 20

puts "nkf --ic=UTF-8-MAC --oc=Shift_JIS"
ga_to_sjis_from_utf8mac = NKF.nkf('--ic=UTF-8-MAC --oc=Shift_JIS', "\u304b\u3099")
puts ga_to_sjis_from_utf8mac.inspect
puts ga_to_sjis_from_utf8mac.encode('UTF-8')

puts "=" * 20

puts "\ufa19 (U+fa19)"
puts "NFD normalized => #{"\ufa19".unicode_normalize(:nfd).inspect}"
puts "NFKC normalized => #{"\ufa19".unicode_normalize(:nfkc).inspect}"
puts "nkf convert => #{NKF.nkf('--ic=UTF-8-MAC --oc=UTF-8', "\ufa19").inspect}"
$ ruby nkf.rb 
が : U+304b U+3099
nkf --ic=UTF-8 --oc=Shift_JIS
"\x{82A9}"
か
====================
nkf --ic=UTF-8-MAC --oc=Shift_JIS
"\x{82AA}"
が
====================
神 (U+fa19)
NFD normalized => "\u795E"
NFKC normalized => "\u795E"
nkf convert => "\uFA19"

ところで、㎝ (U+339D)の変換にも失敗しそうな気がしますが、成功しています。これはどういうことなのでしょうか。 Shift_JISに含まれる文字列の集合はJIS X 0201とJIS X 0208です。このどちらにも1文字で"cm"となる字体は定義されていません。4ではこの「㎝」はどこからやってきたのでしょうか。

「㎝」はNEC特殊文字に含まれており、NECやIBMによるShift_JIS拡張が統合された文字コードであるWindows-31Jに含まれています。これをCP932と呼ぶこともあり5、CP932からUnicodeへの文字変換表には CP932における 0x8770 をUnicodeでの 0x339D に変換すると定義されています。

https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT

Shift_JISの規定において、0x8770 は「保留域」となっています。6 このことからも、JISに規定されているShift_JISにはなく、それの拡張であるWindows-31Jに含まれている文字であることがわかります。7

また余談として、JIS X 0213にて規定されたShift_JISX0213における 0x8770 に「㎝」の字形が含まれています。8

これもnkfで確認することができ、CP932において拡張された文字を扱わないオプション --no-cp932ext を指定することで文字が消えていることが確かめられます。

require 'nkf'
puts "\u339d : U+339d"
cm_to_sjis_from_utf8 = NKF.nkf('--ic=UTF-8 --oc=Shift_JIS --no-cp932ext', "\u339d")
puts cm_to_sjis_from_utf8.inspect
puts cm_to_sjis_from_utf8.encode('UTF-8').inspect
puts "=" * 20
$ ruby nkf.rb
㎝ : U+339d
""
""
====================

nkfでは、事前にNFKC正規化を行ってからでないと正しくShift_JISに変換できないことがわかりました。

iconv

iconvは、以前はRubyの標準添付ライブラリでしたが、2.0で削除されました。

https://www.ruby-lang.org/ja/news/2013/02/24/ruby-2-0-0-p0-is-released/

現在でもgemとしてインストールできるようにはなっていますが、String#encode を使用することが推奨されているので、今回はコマンドラインの結果をみることにします。

https://github.com/ruby/iconv

# iconv.rbとして保存
require 'open3'

puts "\u304c (U+304c)"
puts "String#encode('Shift_JIS') => #{"\u304c".encode('Shift_JIS').inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
  stdin.print "\u304c"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u304b\u3099 (U+304b U+3099)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u304b\u3099".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
  stdin.print "\u304b\u3099"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u0063\u006d (U+0063 U+006d)"
puts "String#encode('Shift_JIS') => #{"\u0063\u006d".encode('Shift_JIS').inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
  stdin.print "\u0063\u006d"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u339d (U+339d)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u339d".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFT-JIS') do |stdin, stdout_e, _|
  stdin.print "\u339d"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

Open3.popen2e('iconv --from-code=UTF-8 --to-code=SHIFTJISX0213') do |stdin, stdout_e, _|
  stdin.print "\u339d"
  stdin.close
  result = stdout_e.read
  puts "Convert to ShiftJISX0213 by iconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('CP932').encode('UTF-8')}"
end
$ bundle exec ruby iconv.rb
が (U+304c)
String#encode('Shift_JIS') => "\x{82AA}"
Convert to Shift_JIS by iconv => "\x82\xAA"
Re-convert to UTF-8 => が
====================
が (U+304b U+3099)
String#encode('Shift_JIS', undef: :replace) => "\x{82A9}?"
Convert to Shift_JIS by iconv => "\x82\xA9iconv: illegal input sequence at position 3\n"
Re-convert to UTF-8 => かiconv: illegal input sequence at position 3
====================
cm (U+0063 U+006d)
String#encode('Shift_JIS') => "cm"
Convert to Shift_JIS by iconv => "cm"
Re-convert to UTF-8 => cm
====================
㎝ (U+339d)
String#encode('Shift_JIS', undef: :replace) => "?"
Convert to Shift_JIS by iconv => "iconv: illegal input sequence at position 0\n"
Re-convert to UTF-8 => iconv: illegal input sequence at position 0
Convert to ShiftJISX0213 by iconv => "\x87p"
Re-convert to UTF-8 => ㎝

nkfと同様に「が」(U+304B U+3099) の変換に失敗している様子がわかります。「か」までの出力には成功していることから、濁点 U+3099 の変換に失敗していそうですね。 またnkfについての説明で触れた「㎝」 (U+339D) については、Shift_JISへの変換は失敗していますが、Shift_JISX0213への変換は成功していますね。

他に指定できそうなoptionもないので、iconvでも事前にNFKC正規化しておく必要がありそうです。

uconv

それでは、Rubyを使用せずコマンドラインから使用できる、Unicodeの正規化形式も扱うことのできるツールはないのでしょうか?

これを行うことのできる uconv というものがあります。これはUnicode Consortiumが保守しているInternational Components for Unicodeというコンポーネント(?)に含まれており、Debianにおいては icu-devtools というパッケージ名で入手できます。

https://packages.debian.org/buster/icu-devtools

uconvに対して -x nfkc というふうに正規化形式を指定する(正確には、適用したいTransliterationを指定する)ことによって、NFKC正規化がされた上で文字コードの変換ができます。

require 'open3'

puts "\u304c (U+304c)"
puts "String#encode('Shift_JIS') => #{"\u304c".encode('Shift_JIS').inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
  stdin.print "\u304c"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u304b\u3099 (U+304b U+3099)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u304b\u3099".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
  stdin.print "\u304b\u3099"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u0063\u006d (U+0063 U+006d)"
puts "String#encode('Shift_JIS') => #{"\u0063\u006d".encode('Shift_JIS').inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
  stdin.print "\u0063\u006d"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8')}"
end

puts "=" * 20

puts "\u339d (U+339d)"
puts "String#encode('Shift_JIS', undef: :replace) => #{"\u339d".encode('Shift_JIS', undef: :replace).inspect}"
Open3.popen2e('uconv --from-code UTF-8 --to-code Shift_JIS -x nfkc') do |stdin, stdout_e, _|
  stdin.print "\u339d"
  stdin.close
  result = stdout_e.read
  puts "Convert to Shift_JIS by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('Shift_JIS').encode('UTF-8').inspect}"
  puts "codepoints => #{result.codepoints}"
end

Open3.popen2e('uconv --from-code UTF-8 --to-code cp932') do |stdin, stdout_e, _|
  stdin.print "\u339d"
  stdin.close
  result = stdout_e.read
  puts "Convert to CP932 by uconv => #{result.inspect}"
  puts "Re-convert to UTF-8 => #{result.force_encoding('CP932').encode('UTF-8')}"
  puts "codepoints => #{result.codepoints}"
end
$ bundle exec ruby uconv.rb
が (U+304c)
String#encode('Shift_JIS') => "\x{82AA}"
Convert to Shift_JIS by uconv => "\x82\xAA"
Re-convert to UTF-8 => が
====================
が (U+304b U+3099)
String#encode('Shift_JIS', undef: :replace) => "\x{82A9}?"
Convert to Shift_JIS by uconv => "\x82\xAA"
Re-convert to UTF-8 => が
====================
cm (U+0063 U+006d)
String#encode('Shift_JIS') => "cm"
Convert to Shift_JIS by uconv => "cm"
Re-convert to UTF-8 => cm
====================
㎝ (U+339d)
String#encode('Shift_JIS', undef: :replace) => "?"
Convert to Shift_JIS by uconv => "cm"
Re-convert to UTF-8 => "cm"
codepoints => [99, 109]
Convert to CP932 by uconv => "\x87p"
Re-convert to UTF-8 => ㎝
codepoints => [34672]

このように、NFKC正規化を行ったうえで、正しくShift_JISに変換できていることが、またCP932を指定したときには正規化を行わなくても「㎝」 (U+339D)を1文字のまま相互に変換できていることがわかります。

ちなみにTransliterationを活用するとこのようにひらがなをローマ字に変換するという面白いこともできます。

$ echo おはようございます | uconv -x '::hiragana-latin;'
ohayougozaimasu

余談 Unicodeにおける正規化形式について

元記事や現在において指定できる正規化形式、NFC、NFD、NFKC、NFKDの4つは、それぞれの名前が規格に登場するのはUnicode 3.0.1からであり、そのリリースは 2000-08-31 です。

https://web.archive.org/web/20050211134342/http://www.unicode.org/unicode/reports/tr15/tr15-19.html

それ以前のリリースにおいては、正規化形式について触れられている記述がなく、前述のUnicode Standard Annex #15から参照できる"Previous Version" においても、 “It is a stable document and may be used as reference material” などの記述が存在しないことから、正規化形式というものが存在するのはUnicode 3.0.1 以降ということになります。

https://web.archive.org/web/20050207015030/http://www.unicode.org/unicode/reports/tr15/tr15-18.html

このあたりの話は、技術評論社から出版されている[改訂新版]プログラマのための文字コード技術入門 の Appendix 4「Unicodeの諸問題」にて詳細に説明されています。この本はとても面白いのでぜひ読んでみてください。

まとめ

あるUnicode文字列に対してNFKCなどの正規化を適用したい場合、Rubyでは String#unicode_normalize にオプションとして、コマンドラインでは uconv を使用することで目的を達成できます。文字コード変換で良く知られるnkfやiconvでは適切に正規化が行われていない文字列を変換することができません。


  1. ではAPFSではどうなのかというと、規格書には j_drec_hashed_jey_t 構造体に格納される name_len_and_hash を計算するときにNFD正規化を行うとこが記載されていますが、ファイル名そのものの正規化についての記述は見付かりませんでした。 https://developer.apple.com/support/downloads/Apple-File-System-Reference.pdf 正規化が行われず、与えられたバイト列をそのまま保持するようになっているのか、同じ名前に見えるファイルを複数作成することができるという記事もあります。 https://eclecticlight.co/2017/04/06/apfs-is-currently-unusable-with-most-non-english-languages/ 

  2. 軽く目を通しましたが、Apple側でこの正規化形式に名前をつけたりはしていないようです。 https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFileSystem/BPFileSystem.html#//apple_ref/doc/uid/10000185-SW1 ただし、Appleの配布しているiconvのencodingには UTF-8-MAC を指定できるので、ほぼ公式だとしていいでしょう。 

  3. https://developer.apple.com/library/archive/qa/qa1173/_index.html より 

  4. JIS X 0208-1997 附属書3 表1より https://www.jisc.go.jp/pdfb6/PDFView/ShowPDF/5gMAAIAz9AGm_33fhhRp 

  5. 「呼ぶこともあり」というのは、当初定められたCP932をいくつかのベンダが独自拡張したあと、それをMicrosoftが統合したWindows-31JのこともCP932と呼ぶからです。現代においてはCP932 = Windows31J としていいと思いますが。 

  6. JIS X 0208-1997 附属書1より https://www.jisc.go.jp/pdfa8/PDFView/ShowPDF/7gIAAKR6fwk8ZKtaZttC 

  7. MSDNに記載されていたCP932の文字一覧より https://web.archive.org/web/20180405180457/https://msdn.microsoft.com/en-us/library/cc194892.aspx 

  8. JIS X 0213-2000 附属書4 表23より https://www.jisc.go.jp/pdfa5/PDFView/ShowPDF/5AIAAHLePslwm6mc6z4g 

Tweet
2020年08月17日
2020年07月19日

unasuke.fmの再始動

unasuke.fm

再始動

https://unasuke.fm を公開しました。ロゴは衰咲ふち(@otoroesaki)さんに作成していただきました。この場を借りてお礼申しあげます。ありがとうございます。

一旦は今までのエピソードを聞けるようにしただけになります。今後、エピソードごとのサマリやshownoteの記載、RSS feedの公開などの改善を進めていきます。

加えて、次の収録についても日程含め検討中になります。

プラットフォームに乗るかどうか

Podcastは、なぜだかまたブームが来ているようで、AnchorやTransistor、stand.fmなどのプラットフォームが登場しています。 プラットフォームに依存することで、労力の削減や便利な機能を使うことができますが、自分はいちから構築することにしました。

自分が発信する場を他人に委ねる怖さ、というものも多分どこかにあって、一度Twitterのアカウントが凍結されたときにはそれはもう気が気ではありませんでした。ブログをMiddlemanでbuildしてS3にホストしているのも、大して有効活用できていないMastodonを維持し続けているのも、そのためかもしれません。

Next.js を選択した理由と、時間がかかった理由

今回は今までの自分の選択であるMiddlemanによる構築ではなく、Next.jsによる構築を選択しました。

理由は、Webサイトをつくるのであれば、Webの一級市民言語であるJavaScriptでつくるほうがいいのではないかという判断、自分の技術の幅を広げたくて、まだ仕事でも触ったことのないReactに慣れておきたかったという学習目的の2つが主です。また、Next.js を選択するのであれば、素JavaScriptよりは、TypeScriptで書くほうがいいと思い、そのようにしました。

そうなると、TypeScriptとReactとNext.jsの3つを初めて触ることになるので、2つのことを同時に学ばない (by ところてん)ようにするどころか3つのことを同時に学ぼうとしてしまっています。 実際、書いては思うように動かず、何度も友人に助けを求めたり、ひたすらWeb上の記事を読んだりして、それでも全然わからず、多忙を言い訳にしてしばらく放置してしまったりしました。

image.png (23.1 kB)

半分エタりかけていましたが、公式ドキュメントを何度も何度も読んでいくうちになんだかある程度「わかる」ようになりました。結局自分は、悩んで泥臭く試行錯誤を繰り返して手を動かさないと技術を身に付けることはできないんじゃないかと思います。

正直まだまだ機能面や見た目の面でも足りていない部分が多く構築中ですが、オリジナルのファイルがSoundCloud上にもう存在しないので、とりあえずWeb上に参照可能な形で復活させることを優先しました。RSSもないのでPodcastとしては片手落ちですが、"Done is better than perfect" と言いますし、まずは公開することにしました。

使用している技術

まず、Next.jsを静的サイトジェネレーターとして採用しました。そうなると必然、Viewを記述するためにReactを採用することになります。また前述のように、主にTypeScriptで記述しています。ESLintとPrettierを用いてコードフォーマットをしています。

音声ファイルのホスティングについては、転送量による課金のないWasabiを、WebサイトそのもののホスティングについてはFirebase Hostingを選択し、DeployはGitHub Actions経由で行うようにしました。確認用にmaster branchをNetlifyにてHostingしています。

また Content Security Policy を有効化にし、Report Onlyの状態にして report-uri.io に収集させるようにしました。

余談ですが、開発はほぼ全てをWindows 10(not WSL)上で行いました。開発環境において特にハマることはありませんでしたが、ESLintのruleに改行コードをUnix styleかWindows Styleかに設定するものがあり、これの扱いが悩ましいところでした。

参考にした記事・コード

Tweet
2020年07月19日
2020年06月30日

パソコンが壊れて、直した話

windows

前提条件

起こったこと

pic.twitter.com/Fm7KgLQe77

— うなすけ (@yu_suke1994) June 28, 2020

まずはこの動画の状況になるまでの出来事を書いていきます。

  1. Windows Updateが来ているので実行する
  2. Windows Updateの途中で何度か再起動される
  3. 再起動のどこかのタイミングで、「Synaptics ドライバーのアンインストールに失敗しました」というエラーメッセージが出て、そこで止まってしまった
    1. 文言はあやふや、今思えばこの写真を撮っておくべきだった。
  4. どのような操作もできないまま固まってしまったので泣く泣く電源ボタン長押しによる中断
    1. 多分これが原因だが、他にどうすればよかったのだろうか。
  5. 再度起動すると、「前回のWindows Updateを修復しています」、のようなメッセージが出る
  6. ログイン画面に遷移するが、一切の操作ができない状態になる
    1. マウス操作不可能、キー入力不可能
  7. 電源ボタン操作で終了、起動し、UEFIメニューから回復オプションに入る
  8. システムの復元ポイントからWindows Updateを実行する前の状態に戻すも状況変化せず
  9. 「インストールされている Windows Update を削除する」を実行するも状況変化せず
  10. 「インストール メディアを使用して PC を復元する」も効果なし
  11. 別のマウス、キーボードを接続するも認識せず
  12. セーフモードで起動するも状況変化せず
  13. セーフモードで起動した結果、UEFIメニューに入れなくなる
    1. 起動直後のキー入力がすっ飛ばされるようになる。これ仕様なの?
  14. 詰み……

一度セーフモードに入ると抜けられなくなるというのは結構な絶望感がありました。

どう直したか

  1. セーフモード画面のまま放置して電池が切れるのを待つ
    1. ここで一旦寝る
  2. 起動し、UEFIメニューに入れること、回復オプションに入れることを確認
    1. これは賭けだったが、入れてよかった。
  3. 「個人用ファイルを保持する」設定で「PC を初期状態に戻す」を実行
  4. キー入力を受け付けるようになり、復活!

という流れだったのでした。イチから環境構築をやりなおしになってしまいましたが、ひとまず直ってよかったです。

※ この記事は復旧させたWindowsのWSL上で書かれました。

Tweet
2020年06月30日
古い投稿