株式会社クリアコード > ククログ

ククログ


OSSプロジェクトへのコントリビュートで避けるべき6つのこと:3. 了承なしに作業を始めない

結城です。

OSSプロジェクトへのコントリビュートの「べからず集」記事について、まだ要領を掴めていない人が「自分のしようとしていることもそれにあてはまるのではないか?」と心配になってコントリビュートをためらうことがないように、具体的な例と考え方を紹介するシリーズの3本目です。前回(2回目)に続き、今回は3つ目の「べからず」として挙げられている、いきなり作業を始めるべきではないという話について説明します。

3. 了承なしに作業を始めない(コンセンサスを得てから開発に着手する)

この記事をご覧になっている皆さんの中には、「実際にコードを書いて、早くOSS開発に参加したい」という思いが強い人もいるでしょう。また、「英語の読み書きは苦手だけれどもコードなら書ける」ということで、端的にコードの提供だけで済ませたい人もいると思います。筆者もかつてはそのように考えていて、言葉での説明を煩わしく感じていました。

ですが、確実に「これは説明も了承もいらない」と言いきれるごく限定的な状況以外では、説明や了承を端折るのはいろいろな意味でハイリスクです。ともすれば、開発に協力したいと思ったプロジェクトの運営に無用な負担をかけることにもなりかねません。そのようなデメリットを伴わず、着実に手堅く開発に参加する方法は、結局は「急がば回れ」ということになります。

業務においてチームで共同作業するときは、すでによほど充分な信頼関係を築き上げられている場合を除いて、チームメンバー間で密にコミュニケーションを取るのが基本です。事前のすり合わせなしに規模の大きな変更を突然見せつけられても、他のチームメンバーはその内容を理解するのに苦労を要しますし、その内容がチームの期待に反していてチームの成果物には取り込めないと判断されてしまうと、その分のすでに行われてしまった作業は無駄になってしまうからです。「了承なしに作業をして、その結果のプルリクエストをいきなり送りつける」というのは、それと同じことなのです。

先にイシューを立てて、こちらの意図をきちんと説明しよう

たとえ1行だけの変更であっても、何の説明もないプルリクエストだけが単発で送られてきては、開発者は戸惑うだけです。

「開発者なんだからプロジェクトの隅々まで完璧に把握してるはずだろう、だったら説明なんかなくてもコードを見れば分かるはずだ」と思いますか?

残念ながら、それは過剰な期待というものです。開発者といえど、ある程度以上の複雑さを持ったソフトウェアでは、どの変更がどこにどういう影響を与えるのかを一目で判断するのは難しいです。変更が予想外の場所で不具合を引き起こす「後退バグ」が起こったり、後退バグの早期検出を図る「回帰テスト」があったりするのは、そのためです。

開発者がプルリクエストの内容をスムーズに理解し、レビューやマージするかどうかの判断を迅速に行えるようにするためには、そのプルリクエストがどんな問題を解決する物なのかの説明が必要です。具体的には、「修正したい現象の再現手順」「現時点で実際に得られる結果」「その変更によって得られるようになる、本来期待される結果」の説明が必要です。

これは、イシューで問題を報告するときの内容と変わりません。ということは、すでにイシューが立てられている問題なのであれば、そのイシューの番号やURLを添えて、「これを修正するための変更です」と書くだけで十分*1ということでもあります。プルリクエストを既存イシューと関連付ければ、経緯や過去の議論をたどりやすくなるメリットも得られます。

そのようなイシューが存在しているのであれば、「私はこの問題の解決に取り組もうと思う」と宣言のコメントを書いて、進捗状況を随時共有しながら作業に取りかかるのがよいです。そうすると、もしプロジェクトオーナーや他の協力者がすでに何か作業の見通しを立てているのであれば、「ちょっと待って欲しい」などのコメントが付くかもしれません。せっかく何かできるチャンスだったかもしれないのに出鼻をくじかれるのは残念ですが、作業が徒労に終わってしまうのを防げたと考えて、別の有意義な作業に取り組むべく頭を切り替えましょう。

なお、そのようにイシューを探す過程で、OSSプロジェクトのイシュートラッカーで「Good First Issue」や「Good First Bug」などとタグ付けされたイシューを見かけることがあるかもしれません。規模の大きなプロジェクトで、プロジェクトに協力してくれる人を随時募っている場合には、新規のプロジェクト参加者が挑戦するのにちょうどいい難易度の課題に、そのようなタグが付けられている場合があります。特に自分自身では何もつまずいていないものの、自分が普段からお世話になっている有名なOSSの開発に参加してみたい、問題解決に取り組んだりプルリクエストを出したりしてみたいという人や、あるいは、自分がつまずいた問題が大きすぎて手に負えないので練習をしたいという人は、そういったイシューの解決に取り組んでみるとよいでしょう。分からないことがあったら、質問をするとプロジェクトに参加中の先達からアドバイスをもらえるかもしれません。

まだ誰も報告していない問題であれば、まずはイシューとして報告します。英語が苦手だからイシューを立てるのは怖い、だからプルリクエストだけで済ませたい、という気持ちは分かりますが、それではかえって開発者に負担をかけてしまったり、悪ければ開発者に無視されてしまったりもします。報告に書く内容のまとめ方英語で説明するときの考え方を参考にしながら、ぜひイシューを立てることに取り組んでみてください。

プルリクエストから始めてもいい場合

プルリクエストはイシューと紐付けるのが原則だと述べたところですが、動作そのものに影響がなく、是非の議論の余地が無い変更の場合、イシューを経ずにプルリクエストから始めても問題無いことがあります。たとえば以下のようなケースです。

  • ローカルな変数名やプライベートな関数名の綴り間違いの修正
  • 言語リソースの誤訳の修正
  • 言語リソースの未翻訳部分に対する訳文の追加
  • Webサイトやドキュメントの誤記の修正
  • Webサイトやドキュメントのリンク切れの修正

こういうものは「再現手順」「実際に得られた結果」「期待される結果」の枠に当てはめにくいので、イシューを立てようと思うと逆に難しいです。実際のプロダクトやドキュメントに残る形でフィードバックをしたいという思いが強い場合には、こういったものから手を着けてみるのもよいかもしれません。

GitHubでは、ログイン済みの状態でファイルの個別のページを表示すると、オンラインで編集を開始できます。他人のリポジトリだった場合、変更を保存するタイミングで自動的に自分のアカウント配下にフォークが作られて、プルリクエストの作成画面になります。GitLabやBitBucketなどのサービスでも、操作の順番は多少異なりますが、同様のことができます。

ただ、この方法での変更結果はコミット前に「テストする」ということが難しいです。そのため、変更時に意図せず文法エラーが混入してしまうこともあり、その変更をマージすると、初期化に失敗して起動できなくなってしまったり、ドキュメントのレイアウトが崩れてしまったり、というようなこともあり得ます。

実際に、筆者が公開しているFirefox用アドオンにおいても、日本語と英語以外の翻訳リソースを提供してもらえることがありますが、文法エラーが残ったままの変更をマージしてしまって、自分の手元の環境で突然動かなくなって焦った、ということが度々ありました*2。Webサイト上からのファイル編集でプルリクエストした場合などには、事後的にでもよいので、どんな変更であっても、必ず一度は自分の手元で動かして(表示して)みて、問題が無いかどうかを確認するようにしましょう

 

また、変数名・関数名・API名といった、動作に影響を与える部分の誤記の修正は、特に慎重に行う必要があります

ローカル変数やプライベートなメソッド・関数は、他から参照されないため、変更しても動作が壊れることはあまりありません。しかし、グローバルな変数や公開のメソッド、モジュール名自体の誤記は、下手に変更すると動作を壊してしまうことがあります。

また、一見するとプライベートな変数や関数であっても、コード片単位でのインクルードや、メタプログラミングによって実行時に外部から参照されるなどのことによって、変更が思わぬ所に影響を及ぼしてしまうリスクがあります。

以上のような事情を踏まえ、実行可能なコードに関わる変更は、原則として、GitHubなどのサイト上から編集して直接プルリクエストするやり方を取らずに、手元で動作に問題が無いことを確認してからプルリクエストを出すようにすることを、強くお勧めします。

 

なお、プロジェクトメンバー間での対面での相談やチャットなど別の場所ですでに議論が尽くされていて、後はコードを変更するだけであった場合に、傍目からは「急にプルリクエストが行われて、何故かすぐにマージされた」という見え方になる場合もあります。

これは経緯を知らない人には区別が付かないので、注意が必要です。「あっちはすぐにマージしてるのに、なんでこっちは放ったらかしにされるんだ!」と思うようなケースに遭遇した場合でも、「ならば自分も」と短絡的には考えないようにしましょう

まとめ

以上、「了承なしに作業を始めない(コンセンサスを得てから開発に着手する)」という原則について、実例を挙げて解説してみました。

この解説は、OSSへのコントリビューションを増やすことを意図した取り組みであるOSS Gateで開催しているワークショップの中で得られた知見をまとめた本、「これでできる! はじめてのOSSフィードバックガイド」の一部を抜粋・再編集した物です。本編ではこのほかにも、問題の報告の仕方やありがちなミス、フィードバック初心者の方が戸惑いがちな点について、なるべく具体例を示しながら、幅広く解説してみています。リンク先では原稿の全文を公開していますが、手元に置いて参照しやすい形式での販売も行っていますので、読書スタイルに合った形式で参照して頂ければ幸いです。

OSS Gateでは、新型コロナウィルスの感染拡大防止の観点から、現在は東京地域を主体としたワークショップをオンライン(Discord)で開催しています。次回開催予定は12月12日(土曜)13:00からで、ビギナー(ワークショップで初めてのフィードバックを体験してみたい人)・サポーター(ビギナーにアドバイスする人)のどちらも参加者を募集中です。ご都合の付く方はぜひエントリーしてみて下さい。業務のチームのメンバーや後輩の方にOSSへのコントリビュートをしてもらいたいと思っている方は、自身をサポーターとして、メンバーや後輩の方をビギナーとして参加のお声がけをして頂くのもおすすめです。

また、当社では企業内での研修としてのOSS Gateワークショップの開催も承っています(例:アカツキさまでの事例)。会社としてOSSへの関わりを増やしていきたいとお考えの企業のご担当者さまは、お問い合わせフォームからご連絡頂けましたら幸いです。

*1 英語では「This fixes the issue #12345.」といった文になります。

*2 そのため現在は、コミットやマージの際に自動的に文法チェックを行うような設定をしていて、マージ前にそれらのミスに気付けるようにしています。

2020-11-04

Flex Confirm MailとRedmine連携のThunderbird 78対応版をリリースしました

結城です。以前に書いたアドオンのThunderbird 78対応状況についての記事の続報です。

  • メール送信前に宛先等を再確認するアドオンFlex Confirm Mailの、Tb78対応版をリリースしました。
    • ただし、自動更新を通じてインストールされた場合の予期しないトラブルが懸念されることから、現時点ではThunderbird Add-ons Webサイト上では公開していません。
    • 使用するには、GitHub上のリリースページからパッケージをダウンロードして頂く必要があります。
    • 旧バージョン(バージョン1系、バージョン2系)の設定は自動的には引き継がれません。お手数ですが、手動操作での再設定をお願いします。
  • メールとRedmineのチケットを関連付けるアドオンRedThunderMineBird PlusのTb78対応版をリリースしました。
    • こちらはThunderbird Add-onsで公開済みとなっております。
    • フォーク元の「Redmine連携」、およびTb68以前での設定は自動的には引き継がれません。お手数ですが、手動操作での再設定をお願いします。

現在世に出ているThundebrirdアドオンの中で、従来から人気があったアドオンの「Tb78対応版」は、WebExtensions Experimentsという仕組みを使った暫定的な対応に留まっている例が多いのが現状です。この手法はアドオンを比較的小さな工数でTb78に対応させられますが、Tb78の次のメジャーリリース以降でアドオンが動作しなくなるリスクがあります。

それに対し、この度リリースしたこれらのアドオンのTb78対応版は、前回記事で述べた「WebExtensions APIに基づいて全面的に再実装する」手法で一から作り直した物となっています。APIの互換性が保たれる限りにおいては、今後のバージョンのThunderbirdでも安定して使い続けられる事が期待できます。

以下、Tb78用のアドオンを開発される方向けの参考情報として、それぞれのアドオンのTb78対応版開発における技術的な特記事項を簡単に紹介します。

Flex Confirm MailのTb78対応

本アドオンの技術的な特記事項は以下の2点です。

  • メールの送信直前に割り込んで、許可・不許可の判断を非同期に行い、許可が得られなければ送信を中断する。
  • 送信しようとしているメールがどういう文脈に属するかを判断する。

1点目の動作は、Tb78から使用可能となった新APIのcompose.onBeforeSendに依存しています。そのため、WebExtensionsベースのFlex Confirm MailはTb68以前のバージョンでは動作しません。

compose.onBeforeSendは、登録したリスナーが返すwebRequest.BlockingResponseと同形式のオブジェクトによって、処理の継続の可否を指定することができます。このとき、リスナーから通常のObjectを返す代わりにPromiseを返すか、同様の効果を得られる物として非同期関数をリスナーにすると、Promiseが解決されるまで(非同期関数が結果をreturnするまで)送信が待機されます。本アドオンではこの仕様を利用し、非同期関数のリスナー非同期的に開いた独自の確認ダイアログでの確認結果が得られてから結果を返すことで、期待される動作を実現しています。

2点目の特記事項の「文脈」とは、ここでは「新規作成」「返信」「転送」「テンプレートからの作成」「下書きの編集」「既存メールの編集」のうちのどの方法で作成されたメールかという意味です。

本アドオンのように「事前に警告するUI」は、あまりに頻繁に警告されすぎると、そのうち警告を読まずに盲目的に許可するようになってしまうジレンマがあります。その一方で、筆者の経験上は、メールの誤送信が発生しやすい場面は「新規にメールを書いた場合や、手動操作で宛先を記入・変更した場合」であるのに対し、実際の運用上では「受信したメールにそのまま返信し、宛先は編集しない」という、宛先内に無関係のアドレスが混入している可能性が低いケースが大半である、という印象がありました。そのため、本アドオンには「安全である可能性が高いケース=既存メールへの返信で宛先が編集されていない場面では警告を行わないようにする」設定があります。

そのような動作を実現するには「返信かどうか」および「宛先が編集されているかどうか」を判別する必要がありますが、compose.onBeforeSendのリスナーにはそのような情報は渡されません。そのため、何らかの方法でこれらを把握する工夫が必要となります。本アドオンでは、メール編集画面で自動実行されるコンテントスクリプトを登録しておき、その中でメッセージを送出して、そのメッセージを受け取ったタイミングで現在開かれているウィンドウを走査したり、メール編集画面にその時点で入力されている宛先を収集したりすることによって、その後のcompose.onBeforeSendのリスナーで必要な判断を行えるようにしています。

RedThunderMineBird PlusのTb78対応

本アドオンの技術的な特記事項は以下の2点です。

  • メールのスレッドとチケットを関連付けて、その情報を保存する。
  • 受信メールの本文を確実に抽出する。

1点目は、そもそもThunderbirdのWebExtensions APIは存在しない「スレッド」という概念をどのように把握するかと、個々のメールに対する任意のメタ情報の紐付けをどのように保存するかという話です。

従来型のアドオンが参照できるThunderbird内部のAPIには、そのものズバリ「スレッドに対して任意の情報を紐付けて保存する」機能があり、従来はこれによって、スレッドの親のメールがチケットに関連付け済みであれば、返信メールについていちいち「チケットに関連付ける」操作をしなくても良いようになっていました。現時点のWebExtensions APIには相当する機能が無いので、すでにある機能の組み合わせで代替しなくてはいけません。

スレッドは、ヘッダの情報から推測できます。messages.getFull()で得られるmessages.MessagePartを参照すると、headersというプロパティですべての生のヘッダを参照できます。その中のreferencesヘッダにスレッドの祖先のメールの'message-id'ヘッダの値が含まれているので、本アドオンではそれを取得してmessage-idをキーとしてチケットのidをIndexedDBに保存しています。こうしておけば、まだチケットに直接関連付けられていないメールでも、スレッドの祖先のmessage-idに関連付けられたチケットのidを取得できる、という具合です。

2点目の受信メールの本文の抽出は、従来アドオンで使用できた「指定されたメールの本文として適切な内容を収集して返す」Thunderbird内部のユーティリティを何らかの方法で代替するという事です。

WebExtensions APIでは、messages.MessagePartpartsとしてマルチパートの各パートを再帰的に辿ることができます。本アドオンでは、その全パートを再帰的に走査して、本文と思われるパートを抽出するようにしました。HTMLメールの本文をプレーンテキストの本文に変換する方法としては、DOMParserでDOMツリーを作り、そのツリーの各ノードを再帰的に走査して、テキスト形式の戻り値を組み立てるようにしました。

2点目の処理は他のアドオンでも必要になる可能性があり、そうなった時にはライブラリとして独立してメンテナンスするようになるかもしれません。

まとめ

以上、Tb78以降のバージョン向けにWebExtensions APIベースで作り直した2つのアドオンのリリースをお知らせし、併せて実装上の工夫もご紹介しました。

今回リリースした2つのアドオンは、どちらも元々は、confirm-mailおよびRedmine連携という、第三者の開発者の方々によって作成されたアドオンでした。既存ソフトウェアに対してお客さまから機能追加の要望を頂いた際、当社では基本的にはその改善をそのソフトウェアの開発プロジェクトにフィードバックする方針としています。ですが、これらについては業務で必要とされる更新頻度がアドオン側の元々の更新頻度と大きく乖離してしまっていたことから、プロジェクトをフォークし、独自の改修版として当社でメンテナンスを行っている状況となっています。

当社ではThunderbirdの法人向けサポートサービスの一環として、ご要望に応じたアドオンの開発や既存アドオンの改修も承っております。今回リリースした2件については、ある程度のニーズが見込まれたことや、社内でも業務上で必要としていたことから、現時点で最も大きな工数がかかるものの将来的なメンテナンスコストを抑えられると考えられる、「Tb78用に完全に再設計する」やり方で対応を行いました。しかしながら、ご要望の要件次第では、多くのアドオンが暫定的な方法として採用しているWebExtensions Experimentsに基づく対応を行うことも可能です。業務上の必要で採用しているアドオンが最新のThunderbirdに対応していないために、脆弱性を抱えている古いバージョンのThundebrirdを使わざるを得ない、という状況でお困りの企業ご担当者の方は、お問い合わせフォームからご連絡を頂けましたら幸いです。

タグ: Mozilla
2020-11-09

LuaのC APIを使ってネストしたテーブル型のデータを作成するには

はじめに

Luaのデータ型の一つにテーブル型があり、ネストさせることができます。
今回は、ネストしたテーブル型のデータをLuaのC APIを使って作成する方法を紹介します。

ネストしていないテーブル型のデータを作成するには

いきなりネストしたテーブルを作成する方法を説明するとややここしくなるので、先にネストしていないテーブル型の場合を説明します。

Luaでprint(inspect(table))した結果、次のように表示されるテーブルをC APIを使って作成します。*1

{
  "cat" = "Meow",
  "dog" = "Bow"
}

このテーブルは次のようなサンプルコードで作成できます。

#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void)
{
  lua_State *L = luaL_newstate();

  luaL_openlibs(L);

  lua_newtable(L);

  lua_pushstring(L, "Meow");
  lua_setfield(L, -2, "cat");

  lua_pushstring(L, "Bow");
  lua_setfield(L, -2, "dog");

  lua_setglobal(L, "table");

  luaL_dostring(L, "print(table.cat)");
  luaL_dostring(L, "print(table.dog)");

  return 0;
}

上記のサンプルは、Cでテーブルを作成して、Luaスクリプトでそのテーブルを参照して表示します。

Luaはスタックを使ってCとデータをやりとりします。そのため、次のような流れで処理する必要があります。

  • lua_newtable(L);でテーブル作成してスタックに積む
  • lua_pushstring(L, "Meow");でスタックに文字列"Meow"を積む
  • lua_setfield(L, -2, "cat");でキーを"cat"、値をスタックに積んである"Meow"としてスタックに積んであるテーブルに設定する

lua_setfieldを呼ぶと、スタックに積まれた"Meow"は除去されます。
-2 はスタックのトップを0としたインデックスを意味します。
つまり、テーブル、"Meow"の順にスタックに積み上げているので、-1は"Meow"を、-2はテーブルを意味します。

ここまでを図示すると次のようになります。

setfieldの例

setfieldを使わない場合(参考)

テーブルを作成するサンプルでは、lua_setfieldを使わずに、lua_settableを使っていることもあります。
lua_settableの場合は、キーと値を積んだ状態で呼びだします。例えば、次のような使い方をします。

  lua_newtable(L);

  lua_pushstring(L, "cat");
  lua_pushstring(L, "Meow");

  lua_settable(L, -3);

lua_settable(L, -3);ではキーが"cat"、"Meow"が値としてスタックに積んだテーブルに設定します。
テーブル、"cat"、Meow"の順にスタックに積み上げているので、-1は"Meow"を、-2は"cat"を、-3はテーブルを意味します。

lua_settableの場合も同様に図示すると次のようになります。

settableの例

ネストしているテーブル型のデータを作成するには

ネストしていないテーブル型のデータの作成方法がわかったところで、本題のネストしたテーブルを作成する方法を説明します。

Luaでprint(inspect(table))した結果、次のように表示されるテーブルを作成します。

{
  "animal" = {
    "cat" = "Meow"
  }
}

このテーブルは次のようなサンプルコードで作成できます。

#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void)
{
  lua_State *L = luaL_newstate();
  int status = 0;

  luaL_openlibs(L);

  lua_newtable(L);

  lua_pushstring(L, "animal");

  lua_newtable(L);
  lua_pushstring(L, "cat");

  lua_pushstring(L, "Meow");

  lua_settable(L, -3);

  lua_settable(L, -3);

  lua_setglobal(L, "table");

  luaL_dostring(L, "print(table.animal.cat)");
  return 0;
}
  • lua_newtable(L);でテーブルを作成してスタックに積む
  • lua_pushstring(L, "animal");でスタックに文字列"animal"を積む
  • lua_newtable(L);でネストしているテーブルを作成してスタックに積む
  • lua_pushstring(L, "cat");でネストしているテーブルのキーとなる"cat"をスタックに積む
  • lua_pushstring(L, "Meow");でネストしているテーブルの値となる"Meow"をスタックに積む
  • lua_settable(L, -3);でスタックからキー(cat)と値(Meow)をとりだし、ネストしているテーブルに設定する
  • lua_settable(L, -3);でスタックからキー"animal"と値(ネストしたテーブル)をとりだし、テーブルに設定する

この場合も同様に図示すると次のようになります。

ネストしたテーブル作成その1

ネストしたテーブルの作成その2

ポイントは、キーである"animal"の値としてネストしたテーブルをlua_settableしていることです。
このようにすることで、多段にネストしたテーブル型のデータを作成できます。

まとめ

今回は、ネストしたテーブル型のデータをLuaのC APIを使って作成する方法を紹介しました。
Luaの言語バインディングを書く機会があったら参考にしてみてください。

*1 inspectはluarocks install inspectでインストールしているものとします。

2020-11-11

OSSプロジェクトへのコントリビュートで避けるべき6つのこと:4. 既知の問題や解決策がある事について報告しない

結城です。

OSSプロジェクトへのコントリビュートの「べからず集」記事について、まだ要領を掴めていない人が「自分のしようとしていることもそれにあてはまるのではないか?」と心配になってコントリビュートをためらうことがないように、具体的な例と考え方を紹介するシリーズの4本目です。前回(3回目)前前回(2回目)に続き、今回は4つ目の「べからず」として挙げられている、既知の問題や解決策がある事について報告しないという話について説明します。

4. 既知の問題や解決策がある事について報告しない(報告する前に、既存の報告や情報がないか探す)

筆者はOSSへの障害報告について、以下の2種類の声をたびたび観測しています。

  • 問題を報告してほしい! 報告がないと、問題の存在を把握できなくて困る!
  • 同じ問題を何度も報告しないでほしい! 重複する問題の報告が多すぎて困る!

一人のプロジェクトオーナーとしては、どちらの気持ちも分かりすぎるくらいに分かるのですが、単純にこのようにそれぞれの声を並べてしまうと、報告しようかどうしようかと迷っている人は、ダブルバインド*1の状態になってしまいます。

なので、この記事では、自分が遭遇した問題を報告してみようと思った人のために、「問題を報告する前に何を(どこを)調べればいいのか」「どこまで調べれば、充分に調べたと言っていいのか」を解説してみます。

困ったときは、まず検索

何はともあれ最初に試みてほしいのは、GoogleDuckDuckGoなどの一般的な検索サービスで、困っている内容を検索してみるということです。OSSプロジェクトのイシュートラッカーを見てみるのは、Web検索でそれらしい情報を得られなかった後からで充分です。

これは、初級者から上級者まであらゆる人の場合に言えることです。上級者の場合でも、自分が遭遇したトラブルについて「これは不具合に違いない、自分が使い方を間違えるわけがない」と思い込んで、いきなりソースコードを調べてみたものの、よくよく調べてみたら単に使い方を間違えていただけだと分かった、というオチになることはよくあります。

検索するときのポイントは、以下の情報を含めることです。

  • 問題が起こる場面でのOSの種類とバージョン、アプリケーション名とバージョン。例:
    • Windows 10 Pro 1909
    • Firefox 82.0.2
  • 問題が起こる場面でした操作に関わる、コマンド列や機能名。例:
    • terraform apply auto-approve(コマンド列とその引数。「-」で始まるフレーズは、検索エンジンによっては「その語を含まない」という意味に解釈されることがあるので、コマンドラインオプションの先頭の「-」は検索語句から外すのが安全。)
    • ファイル 新規作成 既存のメールアカウント(Thunderbirdのメニュー項目の場合。)
    • ip_configuration(変更した設定項目の名前)
  • 問題の発生時に画面上に現れているメッセージ。例:
    • check_is_installed_by_gem is not implemented in Specinfra::Command::Windows::Base::Package(端末上に表示されるエラー出力)
    • このユーザー名とサーバー名のアカウントがすでに存在します。別のユーザー名またはサーバー名を入力してください。(ダイアログに表示されたメッセージ)
  • その他、起こっている症状を説明する表現。例:
    • 動かない(英語ではdoes not workcannot work
    • 開かない(英語ではdoes not openis not openedcannot openなど)
    • エラー(英語ではerror

ブログ記事やSNS上の投稿*2なども含めて、日本語で情報が見つからない場合でも、英語で検索してみるとあっさり答えが見つかることがあります。これは、英語の使用人口は日本語の使用人口より圧倒的に多いので、その分はるかに多くの情報が出回っているからです。英語での表現が分からなければ、Google翻訳DeepL翻訳などの機械翻訳にかけた結果の文を検索してみてもいいでしょう。

当然、見つかった英語の情報を読むときも機械翻訳は有用です。近年では機械翻訳の精度の向上が著しいので、機械翻訳の結果をざっと見るだけでも、必要充分な情報を読み取れる場合が多いです。

既存の報告は、どのくらい探せば充分か?

一度の検索で思うような検索結果を得られなかった場合、検索フレーズを変えて何度か検索し直してみる*3のが常道です。

  • キーワードを類語に入れ換えてみる。例:
    • 開けないインポートできない
    • ボタンが反応しないボタンが無効化されている
  • バージョン情報やベンダー名など、重要度が低いと思われる部分を省略してみる。例:
    • Windows 10 Pro 1909Windows 10 1909Windows 10
    • Mozilla Firefox 82.0.2Firefox 82.0.2Firefox
  • 表示されたエラーメッセージの中で、自分の場合に特有と思われる部分を削除してみる。例:
    • check_is_installed_by_gem is not implemented in Specinfra::Command::Windows::Base::Packagecheck_is_installed_by_gem is not implemented inis not implemented in

ただ、このような試みは、やろうと思えばいくらでもできてしまいます。探している情報が「ある」ことは、一度見つけられれば証明できますが、「ない」ことは証明できません。下手をすれば、存在しない情報を永遠に探し求め続けることにもなり得ます。そうならないためには、「ここまで探したんだからもう見つからない」と諦める判断を下す基準を決めておくのが大事です。

筆者の場合、OSSのトラブルに関する調査では、「自分に思いつく限りの表現で既存の報告を検索してみて、自分の得意分野では10~20分程度、不慣れな分野では30分~1時間程度の間にそれらしい情報が見つからなければ、まだ誰にも報告されていない問題である可能性が高い」と判断するようにしています。疑わしい既存の報告がたくさん見つかった場合は、それぞれの精査にもう少し時間を使うこともあります。いずれにしても、筆者の場合は1時間以上の時間をかけることは希です*4

「重複する報告」や「場違いな報告」を恐れないで!

「Web全体を検索してもそれらしい情報が見つからず、イシュートラッカーのイシュー一覧にもそれらしい報告が見当たらない」という場合には、いよいよ報告のタイミングです。

(なお、「イシュートラッカーに報告するべきか、それともフォーラムでまず聞いてみるべきか」で悩む場合は、それぞれの使い分けを解説する過去記事を先にご覧ください。ここでは、イシュートラッカーに報告する場合と仮定します。)

ただ、「すでに他の人が報告しているにもかかわらず、自分の探し方が下手なだけで、既存の報告を見つけられていないだけかもしれない」と不安に感じて報告をためらってしまう人もいるかもしれません。

そのような場合、前項までで述べたような探し方をもうすでにした後であれば、思い切ってあなたの言葉で新しい報告としてフィードバックすることを、筆者はおすすめします。なぜなら、遭遇した場面や発生時の状況、報告する人の主観によって、同じ問題でも報告の表現の仕方はそれぞれ変わってくるからです。

違う言葉でされた報告は見つからないことがある

同じ原因の問題に関する報告がすでになされていた場合、基本的には後からフィードバックされた方に「重複する報告だ」と印が付けられ、既存の報告へと誘導するような情報が、プロジェクト運営者によって付けられます。

そんな場合でも、「ああ、自分の探し方が悪かったせいで見つけられなかったんだ」なんて恥ずかしく思う必要はありません。何故なら、あなたがした報告自体が、今後同じ探し方をする人のための誘導になるからです。また、重複する報告があまりに多発するようであれば、それはプロジェクトにとっては、問題の影響度合いの大きさのバロメーターにもなります。

重複として処理された報告を手がかりにして、適切な情報に辿り着ける

また、「この問題をこのプロジェクトに報告するのが適切かどうか分からない、もっと適したプロジェクトがあるかもしれない」という不安を感じたケースでも、思い切ってあなたが思った報告先にフィードバックすることを筆者はおすすめします。なぜなら、同じ一つの原因から起こる問題でも、報告する人によって問題に遭遇する経緯やきっかけは違うからです。

異なる経路から同じ原因にたどり着くこともある

代わりの適切な報告先に誘導されたり、開発者側でより適切な先へ報告がエスカレーションされたとしても、「自分の調べ方が甘かった」と凹む必要はありません。先の例同様、あなたの報告は、今後同様の経緯で問題に遭遇した人のための誘導になるからです。

まとめ

以上、「既知の問題や解決策がある事について報告しない(報告する前に、既存の報告や情報がないか探す)」という原則について、実例を挙げて解説してみました。

この解説は、OSSへのコントリビューションを増やすことを意図した取り組みであるOSS Gateで開催しているワークショップの中で得られた知見をまとめた本、「これでできる! はじめてのOSSフィードバックガイド」の一部を抜粋・再編集した物です。本編ではこのほかにも、問題の報告の仕方やありがちなミス、フィードバック初心者の方が戸惑いがちな点について、なるべく具体例を示しながら、幅広く解説してみています。リンク先では原稿の全文を公開していますが、手元に置いて参照しやすい形式での販売も行っていますので、読書スタイルに合った形式で参照して頂ければ幸いです。

OSS Gateでは、新型コロナウィルスの感染拡大防止の観点から、現在は東京地域を主体としたワークショップをオンライン(Discord)で開催しています。次回開催予定は12月12日(土曜)13:00からで、ビギナー(ワークショップで初めてのフィードバックを体験してみたい人)・サポーター(ビギナーにアドバイスする人)のどちらも参加者を募集中です。ご都合の付く方はぜひエントリーしてみて下さい。業務のチームのメンバーや後輩の方にOSSへのコントリビュートをしてもらいたいと思っている方は、自身をサポーターとして、メンバーや後輩の方をビギナーとして参加のお声がけをして頂くのもおすすめです。

また、当社では企業内での研修としてのOSS Gateワークショップの開催も承っています(例:アカツキさまでの事例)。会社としてOSSへの関わりを増やしていきたいとお考えの企業のご担当者さまは、お問い合わせフォームからご連絡頂けましたら幸いです。

*1 矛盾する2つの指示を同時に与えられて身動きが取れなくなること。叱責の場面で黙って叱られていると「なんとか言ったらどうなんだ!」と言われ、抗弁すると「口答えするんじゃない!」と言われる、というような場面はその典型例。

*2 TwitterやFacebookなどのSNSでの投稿は、一般的なWebサイトを対象にした検索サービスでは見つけられないことがあり、そのサービス上で改めて検索する必要があります。

*3 検索サービスによっては、これらに相当することをサービス側で自動的に行った上で結果を返す場合もあります。

*4 筆者の場合、サポートサービスの運用規定によって、決まった工数の範囲内で必ず回答を返さなければいけないから、という前提もあります。

2020-11-12

PostgreSQL Conference Japan 2020:PGroonga 運用技法 ~PGroonga の WAL を放置していませんか?~ #pgcon20j

2020年11月13日(金)にPostgreSQL Conference Japan 2020が開催されます。
私は、「PGroonga 運用技法 ~PGroonga の WAL を放置していませんか?~」という題名で、PGroongaのWALを使う上での注意点を紹介します。

当日使用する資料は、以下に公開しています。

関連リンク:

内容

PostgreSQL で使用できる全文検索の拡張に PGroonga(ぴーじーるんが) という高速で高性能な拡張があります。
PGroongaは、全言語対応の超高速全文検索機能をPostgreSQLで使えるようにする拡張で、安定して高速で、かつ高機能(同義語、表記ゆれや異体字への対応、類似文書検索など)です。

PGroonga はインストールも難しくなく、インデックスの設定もそれほど複雑ではないので、全言語対応の超高速全文検索機能を容易に利用できますが、ストリーミングレプリケーションなどのWALを使用した運用においては注意が必要な点があります。 この講演では、後々トラブルが起きないようにPGroongaのWALをうまく使う方法について紹介しています。

まとめ

この講演は、PGroongaのWALを既に使っている人、または、これから使おうと考えている人向けの内容になっています。
PGroongaのWALを使うことに興味がある方は、是非、発表資料を確認してみてください。

タグ: Groonga
2020-11-13

Software Design12月号のDebian Hot Topicsでインタビュー記事が掲載されます

しばらく前にDebian Developerになった林です。
最近は日本からDebian Developerになった人がいないということで、新Debian Developerへのインタビューを受ける機会がありました。
2020年11月18日発売 Software Design12月号のDebian Hot Topicsのコーナーでちょっととりあげてもらっています。

Software Design12月号の目次にはそれらしい記述はないですけども、掲載されているはずです。
Debianを使い始めたきっかけとか、Debian Developerになって変わったこととかを語っています。

Debian MaintainerやDebian Developerになるための記事を以前書いたので、あわせて参考にしてみてください。

Debianを好きな人は、今週末 11/21(土) にオンラインの東京エリア・関西合同Debian勉強会が開催されるので、ぜひ参加してみてください。

2020-11-18

OSSプロジェクトへのコントリビュートで避けるべき6つのこと:5. squashで1つのコミットにまとめる

結城です。

OSSプロジェクトへのコントリビュートの「べからず集」記事について、まだ要領を掴めていない人が「自分のしようとしていることもそれにあてはまるのではないか?」と心配になってコントリビュートをためらうことがないように、具体的な例と考え方を紹介するシリーズの5本目です。前回(4回目)前前回(3回目)前前前回(2回目)に続き、今回は「べからず」として挙げられている5つ目の点の「squashで1つのコミットにまとめる」ということについて説明します。

5. 途中の複数のコミットをそのままにしない(squashで1つのコミットにまとめる)

「プルリクエストを出す前に、コミットを綺麗に整理しておこう」というアドバイスは時々聞かれます。ただ、どのくらい整理すれば綺麗にしたことになるのか、程度が分からず戸惑う人もいるでしょう。

また、「コミットは細かい粒度で行おう」という原則をリーダブルコードや先輩の指摘などで学んだ人だと、「せっかく細かい粒度でコミットを分けたのに、大きな粒度のコミットにまとめてしまって本当にいいのか?」と不安になるかもしれません。

端的な答えとしては、前者は「rebaseしてからsquashすればそれで充分」、後者は「コミットを分けないと意味が取れなくなるような大きな規模の変更は、そもそもいきなりプルリクエストするのには不適切」と言えます。以下、それぞれもう少し詳しく述べてみます。

コミット履歴をなるべく単純にする

プルリクエスト用のコミット履歴を「綺麗にする」場面で使われるのは、Gitであれば以下の2つのコマンドです。

  • git rebase
  • git merge --squash

rebaseについては、Git公式のrebaseの解説(日本語)で図を交えて分かりやすく解説されています。端的には、「元のブランチの最新の状態からブランチを切って作業したことに歴史を改編する」コマンドと言えます。

Gitに限りませんが、あなたがブランチを切って何か作業をしている間に、元のブランチ側で何か別の作業が行われてコミットが行われている、ということはよくあります。その状態のまま作成したプルリクエストがマージされると、変更の履歴が途中で別れて再合流した形になります。

(図:再合流するコミットグラフ)

このような変更履歴だと、後から「この変更の前後で何が変わり、どのようにして整合性を保ったのか」ということがわかりにくくなる場合があります。

具体例を挙げて説明しましょう。https://github.com/clear-code/example/で開発されているプロジェクトについて、デフォルトブランチがmainで、ファイルの読み込みを行う以下のような関数があったとします(※JavaScriptの構文での擬似コードです)。

function readFile(path) {
  const reader = new FileReader();
  return reader.readSync(path);
}

ここで、あなたがリポジトリをforkした物をローカルにcloneし、git checkout -b workingで作業ブランチを作成して、この関数に対して「ファイルのエンコーディングを指定できるようにする」変更を行ったとします。このときのコミットの差分は以下の通りです。

-function readFile(path) {
+function readFile(path, { encoding } = {}) {
   const reader = new FileReader();
-  return reader.readSync(path);
+  return reader.readSync(path, { encoding });
 }

その一方で、元プロジェクトのmain側で「ファイルを非同期に読み込めるようにする」変更が行われたとします。こちらのコミットは以下の通りです。

-function readFile(path) {
+async function readFile(path) {
   const reader = new FileReader();
-  return reader.readSync(path);
+  return reader.readAsync(path);
 }

このままではworkingブランチからをプルリクエストを作成してもmainにマージできません。先に、あなたのローカルリポジトリのworkingブランチにおいて、git pull https://github.com/clear-code/example.git mainのようにして変更をpullし*1、衝突を解消することにします。衝突した様子は以下の通りです

<<<<<<< HEAD
function readFile(path, { encoding } = {}) {
  const reader = new FileReader();
  return reader.readSync(path, { encoding });
=======
async function readFile(path) {
  const reader = new FileReader();
  return reader.readAsync(path);
>>>>>>> main
}

そして、この衝突を解消してマージを完了すると、マージコミットでの変更は以下のように記録されます。

- async function readFile(path) {
 -function readFile(path, { encoding } = {}) {
++async function readFile(path, { encoding } = {}) {
    const reader = new FileReader();
-   return reader.readAsync(path);
 -  return reader.readSync(path, { encoding });
++  return reader.readAsync(path, { encoding });
  }

さて、この状態で作成したプルリクエストが元プロジェクトにマージされたとします。後からコードを見た人が「このencodingというパラメーターはいつ導入されたのだろうか?」と思うでしょう。行ごとの変更経緯を調べるためにgit blame path/to/fileを実行してみたとしましょう。……すると、なんとパラメーターを実装したときのコミットではなく、マージコミットでの変更しか表示されないのです! ここから作業ブランチでの個別のコミットを辿るためには、リビジョングラフを見ながらブランチ側のログを辿らなくてはなりません。

この例くらい単純だとまだなんとかなりますが、もっと広い範囲に渡って衝突して、マージコミットが巨大になってしまっていたら? あるいは、マージ対象のブランチが単独の作業ではなく複数の作業を含む物だったら? 変更が行われたコミットを特定するのはどんどん困難になってしまいます。調査の度にこのような状況が頻発してしまっては、開発者の負担増は計り知れません。

こういう問題が起こらないように歴史を改編するのが、rebaseです。

(図:合流前に別の変更が無いコミットグラフ)

あなたのローカルリポジトリのworkingブランチにいる状態で、元プロジェクトの最新の状態をpullするときにgit pull --rebase https://github.com/clear-code/example.git mainとすると*2、作業ブランチとmainとの間で変更が衝突していた場合は、このタイミングで衝突を解消するようにコミット内容の修正を求められます*3。先の例であれば、「ファイルのエンコーディングを指定できるようにする」変更を以下のように手動で修正することになります。

-async function readFile(path) {
+async function readFile(path, { encoding } = {}) {
   const reader = new FileReader();
-  return reader.readAsync(path);
+  return reader.readAsync(path, { encoding });
 }

この変更は「衝突をこのようにして回避した」というコミット(マージコミット)ではなく、mainブランチの最新の状態から切り直したブランチに対して行われた、「ファイルのエンコーディングを指定できるようにする」変更として記録されます*4。つまり、履歴上、衝突は発生しなかったことになります

作業ブランチの全コミットが、mainブランチの後に矛盾なく繋がるように修正されれば、rebaseは完了です。この状態からプルリクエストを作成すれば、マージ後の変更履歴を辿りやすくなり、効率よく調査を進められます。

「履歴を綺麗にする」というと漠然としていますが、「衝突を解消するためだけのコミット」が少ない・変更の経緯を一本道で追いやすい状態を維持するということだと考えれば、何をどうすればその状態に近付くか分かりやすいのではないでしょうか。

提案はsquashしても意味を取れる程度の規模にとどめる

squash(スカッシュ)は「押し潰す」という意味の語句*5で、Gitにおいては「複数のコミットを1つにまとめる」操作を指します。

例えば、先の「ファイルのエンコーディングを指定できるようにする」変更を、workingブランチ上で以下の2つのコミットに分けて行ったとしましょう。

 // 1コミット目:オプションを追加
 
-function readFile(path) {
+function readFile(path, { encoding } = {}) {
   const reader = new FileReader();
   return reader.readSync(path);
 }
 // 2コミット目:オプションで指定されたエンコーディングを使用
 
 function readFile(path, { encoding } = {}) {
   const reader = new FileReader();
-  return reader.readSync(path);
+  return reader.readSync(path, { encoding });
 }

これらをsquashしてみます。

squashは、そういう名前のコマンドがあるわけではなく、マージの操作時のオプションで指定します。具体的には、以下の手順で行います。

  1. 作業用のworkingブランチにいる状態から、git branch mainで一旦mainに戻る。
  2. git checkout -b working-to-pullrequestでプルリクエスト用のブランチworking-to-pullrequestを作る。
  3. プルリクエスト用のブランチworking-to-pullrequestにいる状態でgit merge --squash workingと実行する。

このように操作すると、workingにあった複数のコミットが以下のように1つにまとまったコミットになって、working-to-pullrequestブランチの唯一のコミットとして記録されます。

-function readFile(path) {
+function readFile(path, { encoding } = {}) {
   const reader = new FileReader();
-  return reader.readSync(path);
+  return reader.readSync(path, { encoding });
 }

このworking-to-pullrequestブランチからプルリクエストを作成すれば、粒度の細かかった複数のコミットが「1つの大きなコミット」にまとまるため、マージ後の変更履歴がよりスッキリした物になるという訳です。

ですが、前述した通り、これは「作業の意味ごとに細かい粒度でコミットする」原則に反しているようにも思えます。「やらない方がいい」とされていることを、わざわざしてしまって本当にいいのでしょうか?

これは筆者の私見ですが、そもそも、squashした結果のコミットがあまりに複雑になるようなら、提案の規模が1つのプルリクエストにするには大きすぎると言えます。

単発のコントリビュートをする場面では、プロジェクトオーナーとまだ十分な信頼関係が築けていないことが多いでしょう。そのような段階で、squashすると訳が分からなくなるような大規模な変更を受け入れてもらうのは難しいです。レビューに長期間を要したり、あるいは、初手の時点で却下されたりしてもおかしくありません。

そのような段階では、いきなり大きなことに着手するのではなく、小さな規模のプルリクエストを積み重ねて、充分な信頼を得ることから始めるのがおすすめです。

広範囲に渡る大きな規模の変更をどうしても提案したいのであれば、変更を可能な限り小さな規模の、個別の変更に分けて提案しましょう。「Windowsの新しいバージョンへの対応に合わせて、機能Aと機能Bと機能Cを追加し、既存の機能Xの不具合も修正する」といったプルリクエストだったなら、「機能A追加」「機能B追加」「機能C追加「機能Xの修正」と複数回に分ける、といった要領です。

まとめ

以上、「途中の複数のコミットをそのままにしない(squashで1つのコミットにまとめる)」という原則について、実例を挙げて解説してみました。

OSS開発者を増やすことを意図した取り組みであるOSS Gateでは、定期的にワークショップを開催し、「初めてのコントリビュートをしてみたい」人の背中を押す活動をしています。プルリクエストするかどうか迷っているOSSプロジェクトがあるけれども、プルリクエストしていいかどうか自信を持ちきれないという方は、ワークショップに参加してみると、日常的にOSSにコントリビュートしているサポーター参加者からアドバイスをもらえるかも知れません。

OSS Gateでは、新型コロナウィルスの感染拡大防止の観点から、現在は東京地域を主体としたワークショップをオンライン(Discord)で開催しています。次回開催予定は12月12日(土曜)13:00からで、ビギナー(ワークショップで初めてのフィードバックを体験してみたい人)・サポーター(ビギナーにアドバイスする人)のどちらも参加者を募集中です。ご都合の付く方はぜひエントリーしてみて下さい。業務のチームのメンバーや後輩の方にOSSへのコントリビュートをしてもらいたいと思っている方は、自身をサポーターとして、メンバーや後輩の方をビギナーとして参加のお声がけをして頂くのもおすすめです。

また、当社では企業内での研修としてのOSS Gateワークショップの開催も承っています(例:アカツキさまでの事例)。会社としてOSSへの関わりを増やしていきたいとお考えの企業のご担当者さまは、お問い合わせフォームからご連絡頂けましたら幸いです。

*1 あなたの手元のリポジトリ自体が「元プロジェクト」で、ローカルリポジトリ上で作業している場合であれば、`working`において`git merge main`を実行します。

*2 あなたの手元のリポジトリ自体が「元プロジェクト」で、ローカルリポジトリ上で作業している場合であれば、`working`において`git rebase main`を実行します。

*3 衝突が無ければ、矛盾がないように自動的に処理されます。

*4 そのため、rebase後のコミットは元々のコミットとは別物として記録されます。

*5 例えば、飲み物の「レモンスカッシュ」はレモンを押し潰した絞り汁を使うのでそう呼ばれています。

2020-11-20

FluentdでLinuxのcapabilityを処理するには

はじめに

クリアコードはFluentdの開発に参加しています。
Fluentdは主にLinux上やWindows Server上でのユーザーが多いです。
Fluentdの使われ方で特に多いのがLinuxで動いているサーバーのログの取得です。
筆者畑ケがFluentdでLinuxのcapabilityを扱えるようにした話をまとめてみます。
FluentdでLinuxのcapabilityを扱う機能はFluentd v1.12.0に入る予定です。

capabilityとは

Linuxにはcapabilityという権限チェックを部分的にバイパスする機能があります。
この機能は、rootまでの権限は欲しくないけれど、システムの特定のパーミッションがあるように振る舞うユーザーやプロセスが欲しい時に有用です。

例えば、Linuxのsyslogのログファイルが/var/log/syslogにあるとすると、このログに関してはadmグループに属していない通常のユーザーでは読み込めません。

$ ls -lh /var/log/syslog
-rw-r----- 1 syslog adm 60K 11月  5 16:39 /var/log/syslog
$ cat /var/log/syslog
cat: /var/log/syslog: 許可がありません

ここで、rbenvでインストールしているRubyにLinuxのcapabilityの機能の一つのCAP_DAC_READ_SEARCHを付与してみます。

$ sudo setcap cap_dac_read_search=+eip $(rbenv prefix)/bin/ruby
$ filecap $(rbenv prefix)/bin/ruby 
~/.rbenv/versions/2.6.3/bin/ruby     dac_read_search

cap_dac_read_searchのcapabilityを付与したirbで/var/log/syslogを読み込んでみます。

$ irb
irb(main):001:0> File.read("/var/log/syslog")
=> "Nov  5 09:53:11 fluentd-testing anacron[22613]: Job `cron.daily' terminated\n..."

読み込むことができました。

capabilityをRubyから扱うには

LinuxのcapabilityをRubyから扱うにはcapabilityを処理できるライブラリのバインディングを書くのが良いでしょう。

筆者畑ケはlibcap-ngのRubyバインディングを開発しました。

Gemfileに以下のように追記して、

gem 'capng_c'

bundle installをすると:

$ bundle

capng_cをインストールできます。
もしくは、gem installでインストールできます。

$ gem install capng_c

依存するコマンドやライブラリについてはcapng_cのインストール要件をチェックしてみてください。

capng_cを通してcapabilityを確認する

Gemfileに以下を追記してbundle installします。

gem 'capng_c'
$ bundle install

そして、setcap cap_dac_read_search=+eip $(rbenv prefix)/bin/rubyを行ったRubyで動作するirb上でプロセスに付いているcapabilityを確認してみましょう。

irb> require 'capng'
irb> capng = CapNG.new
irb> capng.have_capability?(:effective, :dac_read_search)
=> true
irb> capng.have_capability?(:inheritable, :dac_read_search)
=> false
irb> capng.have_capability?(:permitted, :dac_read_search)
=> true

動いているRubyのプロセスの権限は継承できないようですが、ファイル読み込みの権限がバイパスされるようです。

in_tailでcapabilityを確認する

Fluentdのin_tailプラグインでLinuxのcapabilityを扱えるようにするには、
まず、capng_cを読み込んでいてもいなくても動作するラップするクラスを定義する必要があります。
これは、Fluentdの動作対象の環境はLinuxだけではなく、WindowsやmacOSもあり、また、capng_cは動作時に必須のgemとはしないためです。

fluent/envにLinuxかどうかを判定するメソッドを生やします。

diff --git a/lib/fluent/env.rb b/lib/fluent/env.rb
index 01eba2f6..2b0bf5c8 100644
--- a/lib/fluent/env.rb
+++ b/lib/fluent/env.rb
@@ -28,4 +28,8 @@ module Fluent
   def self.windows?
     ServerEngine.windows?
   end
+
+  def self.linux?
+    /linux/ === RUBY_PLATFORM
+  end
 end

次に、capng_cをラップするクラスを作成します。

diff --git a/lib/fluent/capability.rb b/lib/fluent/capability.rb
new file mode 100644
index 00000000..23f419d5
--- /dev/null
+++ b/lib/fluent/capability.rb
@@ -0,0 +1,87 @@
+#
+# Fluent
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+require "fluent/env"
+
+if Fluent.linux?
+  begin
+    require 'capng'
+  rescue LoadError
+  end
+end
+
+module Fluent
+  if defined?(CapNG)
+    class Capability
+      def initialize(target = nil, pid = nil)
+        @capng = CapNG.new(target, pid)
+      end
+
+      def usable?
+        true
+      end
+
+      def apply(select_set)
+        @capng.apply(select_set)
+      end
+
+      def clear(select_set)
+        @capng.clear(select_set)
+      end
+
+      def have_capability?(type, capability)
+        @capng.have_capability?(type, capability)
+      end
+
+      def update(action, type, capability_or_capability_array)
+        @capng.update(action, type, capability_or_capability_array)
+      end
+
+      def have_capabilities?(select_set)
+        @capng.have_capabilities?(select_set)
+      end
+    end
+  else
+    class Capability
+      def initialize(target = nil, pid = nil)
+      end
+
+      def usable?
+        false
+      end
+
+      def apply(select_set)
+        false
+      end
+
+      def clear(select_set)
+        false
+      end
+
+      def have_capability?(type, capability)
+        false
+      end
+
+      def update(action, type, capability_or_capability_array)
+        false
+      end
+
+      def have_capabilities?(select_set)
+        false
+      end
+    end
+  end
+end

このFluent::Capabilityクラスはcapng_c が正常に読み込まれた際にcapabilityの正確な情報を返しますが、
そうでない場合はスタブされた情報を返します。

in_tailでは、ファイルが読み込み可能かどうかのチェックはFile.readable?(path)で行っており、このメソッドはLinuxのcapabilityについては問い合わせません。
CAP_DAC_READ_SEARCHCAP_DAC_OVERRIDEが有効であれば、ファイルの読み込みに関する権限チェックをバイパスできるので、

diff --git a/lib/fluent/plugin/in_tail.rb b/lib/fluent/plugin/in_tail.rb
index 4c2b8a3d..632e5c7b 100644
--- a/lib/fluent/plugin/in_tail.rb
+++ b/lib/fluent/plugin/in_tail.rb
@@ -22,6 +22,7 @@ require 'fluent/event'
 require 'fluent/plugin/buffer'
 require 'fluent/plugin/parser_multiline'
 require 'fluent/variable_store'
+require 'fluent/capability'
 require 'fluent/plugin/in_tail/position_file'
 
 if Fluent.windows?
@@ -171,6 +172,7 @@ module Fluent::Plugin
       @dir_perm = system_config.dir_permission || Fluent::DEFAULT_DIR_PERMISSION
       # parser is already created by parser helper
       @parser = parser_create(usage: parser_config['usage'] || @parser_configs.first.usage)
+      @capability = Fluent::Capability.new(:current_process)
     end
 
     def configure_tag
@@ -250,6 +252,11 @@ module Fluent::Plugin
       close_watcher_handles
     end
 
+    def have_read_capability?
+      @capability.have_capability?(:effective, :dac_read_search) ||
+        @capability.have_capability?(:effective, :dac_override)
+    end
+
     def expand_paths
       date = Fluent::EventTime.now
       paths = []
@@ -263,7 +270,7 @@ module Fluent::Plugin
           paths += Dir.glob(path).select { |p|
             begin
               is_file = !File.directory?(p)
-              if File.readable?(p) && is_file
+              if (File.readable?(p) || have_read_capability?) && is_file
                 if @limit_recently_modified && File.mtime(p) < (date.to_time - @limit_recently_modified)
                   false
                 else

という変更をin_tailに加えます。
この変更により、glob(*)で指定したファイルパターンの時もcapabilityまでチェックしてOKだったらエラーにならず、tailing対象のパスに加えます。

Linuxのcapabilityを見るようにしたin_tailの動作確認

cap_dac_read_searchを付与したRubyでFluentdを動かすと、パーミッション640のファイルを扱えるようになります。

例として/var/log/syslogを確認してみます:

$ ls -lh /var/log/syslog
-rw-r----- 1 syslog adm 29K Nov  5 14:35 /var/log/syslog

このファイルは通常ユーザーでは読めません。

$ cat /var/log/syslog
cat: /var/log/syslog: 許可がありません

cap_dac_read_searchをRubyの実行ファイルに付けます。
Fluentdが新たに提供するLinuxのcapabilityを操作するfluent-cap-ctlコマンドを使用します:

$ sudo fluent-cap-ctl --add dac_override [-f /path/to/bin/ruby]
Updating dac_override done.
Adding dac_override done.
$ fluent-cap-ctl --get -f /path/to/bin/ruby
Capabilities in '/path/to/bin/ruby',
Effective:   dac_override, dac_read_search
Inheritable: dac_override, dac_read_search
Permitted:   dac_override, dac_read_search

ここでfluent-cap-ctlコマンドを利用したdac_read_search capabilityの付与はsetcap cap_dac_read_search=+eip /path/to/bin/rubyと同義です。
fluent-cap-ctlコマンドは-f fileオプションでファイルを指定しない場合には、
/proc/self/exeをreadlinkして動かしているRubyの実行ファイルへ指定したcapabilityを自動で付与します。

そして以下のFluentdの設定ファイルを用意します:

<source>
  @type tail
  path /var/log/sysl*g
  pos_file /var/run/fluentd/log/syslog_test_with_capability.pos
  tag test
  rotate_wait 5
  read_from_head true
  refresh_interval 60
  <parse>
    @type syslog
  </parse>
</source>

<match test>
  @type stdout
</match>

positionファイルを配置するディレクトリを作成し、パーミッションを調整します:

$ sudo mkdir /var/run/fluentd
$ sudo chown `whoami` /var/run/fluentd

これで、通常ユーザーでcap_dac_read_searchの付いたRubyを使ってFluentdを実行すると:

$ bundle exec fluentd -c in_tail_camouflage_permission.conf
2020-11-05 14:47:57 +0900 [info]: parsing config file is succeeded path="example/in_tail.conf"
2020-11-05 14:47:57 +0900 [info]: gem 'fluentd' version '1.12.0'
2020-11-05 14:47:57 +0900 [info]: gem 'fluent-plugin-systemd' version '1.0.2'
2020-11-05 14:47:57 +0900 [info]: using configuration file: <ROOT>
  <source>
    @type tail
    path "/var/log/syslog"
    pos_file "/var/run/fluentd/log/syslog_test_with_capability.pos"
    tag "test"
    rotate_wait 5
    read_from_head true
    refresh_interval 60
    <parse>
      @type "syslog"
      unmatched_lines
    </parse>
  </source>
  <match test>
    @type stdout
  </match>
</ROOT2
2020-11-05 14:47:57 +0900 [info]: starting fluentd-1.12.0 pid=12109 ruby="2.6.3"
2020-11-05 14:47:57 +0900 [info]: spawn command to main:  cmdline=["/home/fluentd/.rbenv/versions/2.6.3/bin/ruby", "-rbundler/setup", "-Eascii-8bit:ascii-8bit", "/home/fluentd/work/fluentd/vendor/bundle/ruby/2.6.0/bin/fluentd", "-c", "example/in_tail.conf", "--under-supervisor"]
2020-11-05 14:47:58 +0900 [info]: adding match pattern="test" type="stdout"
2020-11-05 14:47:58 +0900 [info]: adding source type="tail"
2020-11-05 14:47:58 +0900 [info]: #0 starting fluentd worker pid=12143 ppid=12109 worker=0
2020-11-05 14:47:58 +0900 [info]: #0 following tail of /var/log/syslog
2020-11-05 09:53:11.000000000 +0900 test: {"host":"fluentd-testing","ident":"anacron","pid":"22613","message":"Job `cron.daily' terminated"}
2020-11-05 09:53:11.000000000 +0900 test: {"host":"fluentd-testing","ident":"anacron","pid":"22613","message":"Normal exit (1 job run)"}
2020-11-05 09:55:01.000000000 +0900 test: {"host":"fluentd-testing","ident":"CRON","pid":"24610","message":"(root) CMD (command -v debian-sa1 > /dev/null && debian-sa1 1 1)"}

Fluentdは許可がありません、というエラーを吐かなくなります。
このことから、in_tailで通常ユーザーが読めないファイルをLinuxのcapabilityを見てあげることによって、
権限チェックをバイパスして通常ユーザーが読めないファイルを読めるようにできることがわかります。

まとめ

FluentdでLinuxのcapabilityを扱えるようにした作業で行ったことを解説しました。
Linux capabilityをFluentdに同梱されるコマンドのfluent-cap-ctlにて変更したり削除したりすることも併せてサポートしました。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。

タグ: Fluentd
2020-11-27

OSSプロジェクトへのコントリビュートで避けるべき6つのこと:6. 意義のある報告や提案をする

結城です。

OSSプロジェクトへのコントリビュートの「べからず集」記事について、まだ要領を掴めていない人が「自分のしようとしていることもそれにあてはまるのではないか?」と心配になってコントリビュートをためらうことがないように、具体的な例と考え方を紹介するシリーズの6本目です。前回(5回目)前前回(4回目)前前前回(3回目)前前前前回(2回目)に続き、今回は「べからず」として挙げられている6つ目の点の「意義のある報告や提案をする」ということについて説明します。

6. 無意味な変更を提案しない(意義のある報告や提案をする)

このシリーズの発端になった元記事自体、Tシャツ目的のspamプルリクエストに嫌気がさした人によって書かれた物だったことから、元記事の6項目めはシンプルに「そういうspam行為をするな!」という話になっています。

そのこと自体には特に付け加えることも無いのですが、それだけだと話が終わってしまうので、ここではもう少し一般的な話に広げて、「どのような変更の提案なら、してもいいのか」「変更を提案するときは、どういう切り口で提案すればよいか」を説明することにします。

プロジェクトオーナーが「この変更を待っている」と明示している物は狙い目

OSS開発プロジェクトのイシュートラッカーを見てみると、「Good First Issue」のようなタグが付けられている例や、開発者自ら「誰かこれを実装してくれませんか?」と協力者を募っている例が見つかる場合があります。

特に提案してみたいアイデアは無いけれども、腕試しやコードへの慣熟、開発者に対する信頼の積み増しがしたい、あるいはもっと単純に、純粋に開発という行動を通じてそのプロジェクトに協力してみたい、という人は、こういったイシューから作業に取り組んでみるのがおすすめです。というのも、プロジェクトオーナーが自ら表明しているということは、これらのイシューに基づいてプルリクエストをする分には「どんな変更なら有意義か?」で悩む必要がないからです。このような場合、コードの出来に大きな問題が無ければ、すんなりマージしてもらえる可能性は高いでしょう。

やりたいことがはっきりしている場合でも、このような切り口でコントリビュートを重ねることには、プロジェクトオーナーからの信頼を積み重ねられるというメリットがあります。「この人は変な事をしない」「この人はプロジェクトの事情をよく分かってくれている」と認知してもらうことで、プロジェクトの方針自体の見直しが必要になるような大きな変更の提案でも、受け入れてもらえる可能性が出てくる場合があります。

過去のイシューから提案の採用傾向・却下傾向を見る

自分から新たにイシューを立てて、プロジェクトに対して何か提案をしてみたい、という場合でも、似たような問題を取り扱ったイシューが無いかどうか、過去の例を調べてみるのは有効です。たとえば、何か新しいファイル形式への対応を提案しようとしているなら、別のファイル形式に対応したときの提案を見ると、提案の仕方を考える上で大いに参考になります。

もし類似の事例で却下されたイシューやプルリクエストがあった場合、やりとりを見ると却下の理由が分かる場合があります。自分がこれからしようとしている提案が、そのイシューやプルリクエストの却下理由と同じ要素を含んでいるのであれば、提案内容を見直すヒントになるでしょう。やり取りの中で「こういう情報が欲しい」のような形で必要な判断材料に言及されている場合、提案時点でそれらを揃えておくと、プロジェクトオーナー側の負担が減って、提案がより採用されやすくなるかもしれません。

プロジェクトの目的やスコープ、方針を確認する

自分が提案しようとしている内容について、参考にできる過去のイシューやプルリクエストが存在しない場合には、提案内容がプロジェクトの趣旨に合致しているかどうかを自分で判断する必要があります。

プロジェクトの趣旨は一般的には、プロジェクトの公式サイトや、GitHub上のリポジトリであれば「README」ファイルで説明されていることが多いです。特に、「このプロジェクトは何々を目的としています」「このプロジェクトは何々は目的としていません」といった分かりやすい書き方でプロジェクトオーナーの意思が表明されている場合には、その内容が大いに参考になります。

そのようなはっきりした書き方がなされていない場合や、単に「このソフトウェアは何々をします」というソフトウェアの現状の説明しかなされていない場合には、周辺的な情報からプロジェクトオーナーの意志を推測する必要があります。わかりやすい判断材料の例をいくつか挙げてみます。

  • 実行時のプラットフォーム(Windows、macOS、Linuxなど)や開発言語(JavaScript、Ruby、Pythonなど)、表示メッセージやドキュメントの言語(日本語、英語、中国語など)について、特定のケースの情報ばかりが多く書かれていて、他のケースへの言及がない場合、そのケースに特化することがプロジェクトの趣旨の一部である可能性がある。
    • もしそうであれば、別のケースへ対応するための変更は、趣旨にそぐわないために採用されない可能性が高い。例えば、「位置情報を扱うRubyのライブラリ」の開発プロジェクトに対して「Pythonでも使えるようにして欲しい」という要望・提案は、前提となる開発言語がそもそもマッチしないため、恐らく採用されない。
    • ただし、他言語対応のソフトウェアで表示メッセージが英語しか用意されていないようなケースでは、日本語など他の言語での表示メッセージ定義を提供する(翻訳する)変更は採用してもらえる可能性が高い。
  • コミット数が多い、最初のコミットが古いなどの場合、プロジェクトの歴史が長いと判断できる。
    • もしそうであれば、プロジェクトの方針が明確に固まっているために、現行の方針から大きな変化が必要となる変更は採用されない可能性がある。
    • 逆に、プロジェクトの歴史が浅い場合には、プロジェクトの方針が固まっていないために、ドラスティックな変更でも採用される可能性がある。
  • 知名度が高いソフトウェアや、多くのソフトウェアからライブラリとして使用されているソフトウェアである場合、(特に歴史が長ければ長いほど)後方互換性を重視している可能性が高い。
    • もしそうであれば、既定の挙動を変更する提案は、後方互換性を損なうため採用されない可能性が高い。
    • 新機能の提案であっても、既存の機能と著しく使い勝手が異なる機能の提案は採用されない可能性がある。
    • 提案時に、既存の機能・既定の挙動が損なわれていないことを確認する自動テストを含めておくと、プロジェクトオーナーにとっては「変更をマージして大丈夫かどうか」の判断のコストが減るため、採用される可能性が高まると考えられる。
  • 動作を変えるための設定・指定について、利用者側での詳細な指定ができず、細かい部分は自動判別するようになっている場合、「設定より規約」の方針を取っている可能性がある。
    • もしそうであれば、単純に設定項目を追加する変更の提案は採用されない可能性が高い。
    • 設計方針を合わせて、必要な動作を全自動あるいは半自動で判別できるようにすると、提案を採用してもらえる可能性が高まると考えられる。

ただ、これらの例を踏まえた提案であっても採用されない場合はありますし、上記の例に当てはまらないイレギュラーな変更の提案でも、プロジェクトオーナーの判断によっては採用される可能性はあります。基本的には、提案時にはいきなり作業に取りかかるのではなく、まず「このような変更はどうか?」とプロジェクトオーナーの判断を仰ぐ所から始めるようにしましょう。

まとめ

以上、「無意味な変更を提案しない(意義のある報告や提案をする)」という原則について、実例を挙げて解説してみました。

OSS Gateでは、新型コロナウィルスの感染拡大防止の観点から、現在は東京地域を主体としたワークショップをオンライン(Discord)で開催しています。次回開催予定は12月12日(土曜)13:00からで、ビギナー(ワークショップで初めてのフィードバックを体験してみたい人)・サポーター(ビギナーにアドバイスする人)のどちらも参加者を募集中です。ご都合の付く方はぜひエントリーしてみて下さい。業務のチームのメンバーや後輩の方にOSSへのコントリビュートをしてもらいたいと思っている方は、自身をサポーターとして、メンバーや後輩の方をビギナーとして参加のお声がけをして頂くのもおすすめです。

また、当社では企業内での研修としてのOSS Gateワークショップの開催も承っています(例:アカツキさまでの事例)。会社としてOSSへの関わりを増やしていきたいとお考えの企業のご担当者さまは、お問い合わせフォームからご連絡頂けましたら幸いです。

2020-11-30

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|