その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

一定のインターバル期間中に stdin に何かが来る、あるいは来ない時に任意のコマンドを実行するツール "conk" を書いてた

github.com

これ去年書いたツールなのでいつの話をしておるのかという感じですけれども、一定のインターバルの間に stdin 経由で何かデータが来た時あるいは来なかった時に任意のコマンドを実行するツールを書いていました。

使い方の例としては

$ ./do_something | conk --interval-sec 5 \
  --on-notified-cmd '["echo", "notified"]' \
  --on-not-notified-cmd '["echo", "not notified"]' \
  --on-ticked-cmd '["echo", "ticked"]'

という風にしてあげると、5秒周期で評価が走り、その周期内に do_something からパイプ経由で何かデータを渡された時には echo notified が、何も来なかった時は echo "not notified" が実行され、そして来ようが来まいが echo ticked は毎度実行されるという挙動をするようになっております。
README のビデオを見てもらうとまさにそのような挙動を確認することができます。


たまにこういうのが欲しくなるので書いた、みたいな感じでした。

openapi-generator を使って go のファイルを生成した時に出てくる `var _ context.Context` の謎

openapi-generator を使って go のコード生成をすると、

// Linger please
var (
	_ context.Context
)

という一見不要そうな変数宣言がコード中に出てきます。これはなんなのでしょうか? 「残しておいてね」とは書いてありますが……

TL;DR

たぶんこれ過去のワークアラウンドがそのまま残っている感じだと思われます。まあ特に気にする必要もなさそう……

<追記>
このワークアラウンドの残滓を消すパッチを送ってみた。
github.com

^ 送ってみたところマージされたのでこの謎は過去のものになりつつあります。
</追記>

詳しく見てみる

というわけでコードを掘ってみましょう。リビジョンについては今日 (2022-02-16) 現在の最新である 986446c1d5e7b2c16c667ed18b6c95b2679268f5 から遡っていきます。

なるほど、ここに該当するコードがありますね。blameしていきましょう。

openapi-generator/modules/openapi-generator/src/main/resources/go/api.mustache の一番古いコミットを見てみるとここになるようです:

ここにもこの変数宣言はまだありますね。つまりこれが最古のコミットで、そこに「ある」ってことは「ある」んだよ、ガタガタ言うな、ということでしょうか……
とまあそうではなく、というのもこのファイルは openapi-generator/modules/swagger-codegen/src/main/resources/go/api.mustache から改名されているので一見すると歴史が途絶えてしまっているようですが、これよりも古いコミットはちゃんとあります。更にこのファイルの歴史を辿ってみましょう。
しかし swagger から openapi に名前が変わっているのは歴史を感じますね。

で、掘っていくとこのコミットに行き当たります: 3ed1aa8e79687bed63dfa064de27317273080471

この diff を見ると、件の変数宣言はここが始祖のようです。それはそうとして golang.org/x/net/context が使われていたりしてなんだか懐しいですね。

で、このテンプレートファイルを読んでみると {{#hasAuthMethods}}ctx context.Context, {{/hasAuthMethods}} という条件分岐があるのがわかります。
つまり、この条件が false になるとこの部分が欠落し、そうなった時に import "golang.org/x/net/context" があるとコンパイラに未使用の import があると怒られるので、そのワークアラウンドに無名変数 (_ って無名変数って名前で良いんでしたっけ) として context.Context を変数宣言することで import 未使用コンパイルエラーを回避しているように読みとれます。なるほど〜。 *1


さて翻って最新のコードを再び見てみましょう。

このコードを読むと context.Context は常に使われているように見えるし、わざわざ var _ context.Context を付けておく必要もないように見えます。これパッと見不要そうに思えるんですが、今でも残しておく必要あるんですかね? 後でパッチでも投げてみようかしら……

結論

// Linger please
var (
	_ context.Context
)

はかつてのワークアラウンドの名残 (だと思われる)。

*1:とはいえコミットログにその旨は書いていない。コミットログの issue 番号も恐らく前の repository のものなのでどれと紐付いているかわからない、という感じなのであくまでコードを読んだ上での推察です

API越しでタイムスタンプをやりとりする時のフォーマットをどうするべきか

APIのリクエストにせよレスポンスにせよ、タイムスタンプを利用するというのはよくある話です。
この時、そのタイムスタンプのフォーマットをどうするのが良いのかという話題です。IDLを使って縛るというというのは良い考えだと思いますが、IDLを使うにせよフォーマットについては決めなくてはならないので。

1. 文字列を使う

これあんま良くないと思うんですよね……というのも、とあるAPIを触っている時に「タイムスタンプはRFC3339です」というフィールドがあったんですけれどRFC3339ではないフォーマットで返却されたり受け入れられたりしたのであまり信用ができない……
まあフォーマットが不正というのは極端な例かもしれないですが、仮にフォーマットが不正だと多くの場合 strptime()time.Parse() なんかの時刻文字列のparserが正しく動かず (良いケースだとエラーが上がる、悪いケースだと本来意図しない時刻として扱われる。後者が大きな問題) システムが壊れると思うのでunix timeを使ったほうが良いと考えています。あといまだにstrptime/strftimeの変換指定が満足に覚えられない!

あとタイムスタンプ文字列だと無邪気にタイムゾーン情報が欠落してたりすることがあるんで、それもunix timeを使ったほうが良いと考える一因です。まあちゃんとタイムゾーン情報を付けてくれれば良いんですけど。

別解

ハハハ

2. unix timeを使う

まあこれが穏当なんじゃないですかね、UTCであることも明確だし……と思いきやこういった落し穴が。

これについては2つあると思っていて、時間の解像度 (単位) についてはフィールドに _sec とか _ms のような接尾辞を付けることで明確にするように心がけています。本質的にミスを防げるかというと怪しいですが、無いよりは良いでしょう、という考えです。

もう一つの「整数か小数か」については型として縛るというのが方法じゃないでしょうか……IDLでやるのが良いのでしょうね。

3. TAI64を使う

https://cr.yp.to/libtai/tai64.html

やった、一意なフォーマットだ!!!
とはいえ人間が基本的には読めない、うるう秒の扱いが特殊、など色々ありこれはこれで困りそうですね。

果たしてそうだろうか?

現時点での個人的な見解

  • unix timeを使う
  • 分解能は何らかの方法で明示する
  • IDLを使う

というあたりが現実的な落しどころじゃないでしょうか。他になにか、APIのリクエスト・レスポンスに使えそうな先鋭的な時間表現ってあるんでしょうか?

[追記]


[追記ここまで]

実行中のシェルスクリプトをchattr(1)を使ってimmutableにするというのはどうか

[追記]

実行中のシェルスクリプトをchattr(1)を使ってimmutableにするというのはどうか - その手の平は尻もつかめるさ

調べてみたけどこれが良さそう <a href="https://stackoverflow.com/a/3399850/1921216" target="_blank" rel="noopener nofollow">https://stackoverflow.com/a/3399850/1921216</a>

2022/01/02 17:02
b.hatena.ne.jp
このブックマークコメントで指摘されましたが、immutableにするまでもなくこのラッパースクリプトを噛ませると良さそう。

#!/bin/bash

# usage:
#   sh-run.sh script-you-want-to-run.sh args...

set -ue

file="$1"
script() {
        source "$file"
}

script ${@:2:($#-1)}

[追記ここまで]


sh/bashで実行したシェルスクリプトを実行中に上書きするとシェルスクリプトが再読み込みされ、意図しない挙動をするという現象が昨年末に話題になりました。

bash は、シェルスクリプトの実行中に適時シェルスクリプトを読み込みます。この挙動による副作用を認識できておらず、実行中のスクリプトが存在している状態でスクリプトの上書きによりリリースしてしまったことで、途中から修正したシェルスクリプトの再読み込みが発生し、結果的に未定義の変数を含む find コマンドが実行されてしまいました。
https://www.iimc.kyoto-u.ac.jp/services/comp/pdf/file_loss_insident_20211228.pdf

これは簡単に挙動を確認できます。例えば以下のようなシェルスクリプト script.sh を用意して、

#!/bin/bash

sleep 10
echo "hello"

bash script.sh として実行します。そしてsleepしている10秒間に

#!/bin/bash

sleep 10
echo "yo"

などと書き換えると再読み込みが実施され、sleep終了後にはstdoutに hello ではなく yo が表示されることなります。
今回は単純に「echoする内容」を書き換えただけなので再読み込み後の挙動はわかりやすいものでしたが、しかし大規模にスクリプトを書き換えた際にそれがどのように振る舞うかは予測が難しいものになります。


この実行中の再読み込み挙動は多くの場合望ましくないものだと思います。
というわけで実行中にchattr(1)を利用してimmutableにすることで、ファイル内容の変更や削除を防ぐというのはどうだろうか、という提案です。
例えば、

sudo chattr +i script.sh

としてあげると script.sh はimmutableとなり、ファイルの削除や名前変更、ファイルの書き込みなどが禁止されます。

it cannot be deleted or renamed, no link can be created to this file, most of the file's metadata can not be modified, and the file can not be opened in write mode.
https://man7.org/linux/man-pages/man1/chattr.1.html

すなわち実行したいシェルスクリプトファイルをimmutableにしてあげると、そのファイルに対する変更が禁止されるため意図しない再読み込みも防ぐことが可能となります。安全そうですね。
従って、以下のようなシェルスクリプトを実行するためのラッパースクリプトを噛ませることで「シェルスクリプトに自動的にchattr +iしつつ実行する」というふうにしてやるとオペミスを防げて安全かつ便利なのではないかと思いました。

使い方の例: sh-run.sh script-you-want-to-run.sh args...

chattrでimmutable属性を付与するには実行ユーザーがrootあるいはCAP_LINUX_IMMUTABLE capabilityを有している必要があります。なので上記のラッパーではsudoを付けてchattrを実行しています。


自分はシェスクリプトで書かれた長めのバッチ処理を実行することがしばしばあるので、この運用を試しにやってみようかなと思います。
なお、zsh script.sh などとしてbashあるいはsh以外のシェル処理系で実行する場合はこのような再読み込み挙動は発生しません。zsh等をalternativeとして使うというのも一つの防護策かもしれないですね。

FAQ

ラッパースクリプトが途中で書き換えられたらどうなるの?

このラッパースクリプトにもchattrでimmutable属性を付けておけば良いのでは?

古いaws/aws-lambda-goでAWS Lambdaのcontainer image runtimeを使うと刺さる

具体的に言うと、aws/aws-lambda-go@v1.18.0よりも前のバージョンでAWS Lambdaのcontainer image runtimeを使うとハンドラが呼び出されず、タイムアウトするまで刺さります。

例えば以下のような非常に簡単なLambda Functionをデプロイした時、

package main

import (
	"context"
	"fmt"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func handler(ctx context.Context, event events.DynamoDBEvent) error {
	fmt.Println("CALLED")
	return nil
}

func main() {
	lambda.Start(handler)
}

古いaws/aws-lambda-goを利用していると、以下のログのように CALLED は表示されずタイムアウトまで実行を引っぱります。つまり handler() が呼ばれることなくタイムアウトをしているという挙動であることがわかります。

START RequestId: ...snip... Version: $LATEST
END RequestId: ...snip...
REPORT RequestId: ...snip...	Duration: 3003.52 ms	Billed Duration: 3000 ms	Memory Size: 128 MB	Max Memory Used: 10 MB	
2021-12-20T02:49:56.683Z ...snip... Task timed out after 3.00 seconds

このライブラリのバージョンをv1.18.0以降のバージョンにする *1 と、しっかりとハンドラが呼ばれて意図通りの動きをします。

START RequestId: ...snip... Version: $LATEST
CALLED
END RequestId: ...snip...
REPORT RequestId: ...snip...	Duration: 9.12 ms	Billed Duration: 924 ms	Memory Size: 128 MB	Max Memory Used: 26 MB	Init Duration: 914.31 ms	

go1.xで既に動いているLambda FunctionをそのままContainerに移植してみたところ、この問題で絶妙にハマってしまいました。Lambdaの実行ログには特にそれらしいエラーメッセージも出てこないし、そもそもハンドラ関数が呼ばれないのが謎すぎた……。


それはそうとしてv1.18.0のリリースノートにはそれらしい変更について言及が無いんですが、どれが該当する変更なんでしょう。これなのでしょうか? https://github.com/aws/aws-lambda-go/pull/298
[追記]
上記の件はやはりこのPull Requestが関連するとのこと https://github.com/aws/aws-lambda-go/pull/298


[追記ここまで]

さておきライブラリのバージョンは上げないと駄目ですね。ヤル気があるので最新まで上げました。以上です。

*1:つまりv1.18.0は大丈夫

テクノブレーン被害者アドベントカレンダー Day 19

この記事はテクノブレーン被害者アドベントカレンダーの19日目として書かれています。このアドベントカレンダーは今まさに作りましたから、参加者は自分しかいません。他に被害者がいたら続きを書いておいてください。
この記事は特定の企業に対する苦情および批判が含まれます。お前だ、テクノブレーン

こんなことが横行していては、「リクルーティング」という職業の価値が著しく毀損されてしまうし、ソフトウェアエンジニアリング産業自体がスポイルされていってしまう。

明確に、俺は強く怒っている。お前たちは「駄目」だ。

TL;DR

  • テクノブレーンは本当に悪質なリクルーティング企業なので使ってはなりません。
  • テクノブレーンから電話が来ましたか? 奴らはカモフラージュしてきますが相手をしてはいけません。
  • テクノブレーンを貴方の所属する企業が採用目的で利用していますか? こんな邪悪な企業を使っているようでは自身の会社も邪悪だと思われますよ。少なくとも俺は軽蔑します。使ってはなりません。

本題

こういったことが、自分の把握している限り今年に入ってから5件以上発生しています。

だいたい、以下のパターンです。

  • なんらかの有名企業の名前が付いた子会社っぽい社名を名乗ってきて、「用事があるから」として所属企業の代表番号に電話をかけてきて、折り返しの電話を要求してきます。
    • この際、詳細な要件を追及すると一切話してきませんから、その時点で怪しいです。
    • 上記の場合は「トヨタITソリューションズ」という社名を名乗っていますが、そんな会社は実在しません。トヨタはもっとしっかりした、素晴しい企業です、GR 86とか作ってる最高の企業なんですよ。連中は単に「トヨタ」という接頭辞を付けて「それらしい」会社名をでっちあげて、電話の折り返しの確率を上げているだけだと考えられます。
  • 携帯電話の番号で電話をかけてきます。タチが悪いですね。
  • 仮に折り返しの電話をかける際、非通知設定、Skype、Twilioなどから電話をかけると取ってくれません。悲しいですね。俺は闘う準備ができているのだぞ。


と、こんな感じです。こんな電話は決して取り継がないでください。
もちろん、折り返しの電話は皆さん絶対にかけないでください! しかしあえて電話をかけてみると……*1

相手「もしもし」 (ここで決して自身が何者かは名乗らない)
僕「もしもし、電話を頂いた id:moznion (本当は本名を名乗っている) ですけど」
相手「はい」
僕「どういったご要件ですか、トヨタITソリューションズさんとのことでしたが、あなたは誰ですか」
相手「わたくし、テクノブレーンの……」
僕「アッ、ハイ、もう大丈夫でs」
(ここでブツッと電話が切れる)

という感じで、こちらが真摯に拒否しようとしているのにも関わらず途中で電話をブチ切りしてきます。無礼にも程がありますね。偽名だろうが、ナカガワ、貴様だ。

まとめ

テクノブレーン及び類似の業者に告ぐ。

  • 代表電話で取り継いでくれるメンバーも、俺も、お前のように暇ではない。クソみたいな電話をかけてくるんじゃねえ。
  • これ以上、同じ手口でリクルーティングをしかけてくるようであれば相応の対応を検討するのでそのつもりで。
    • ご提案:


人類全員、誰もテクノブレーン及び類似の業者を、求職、転職、その他あらゆる目的で利用してはなりません。本当にゴミカス。
リクルーティングというのは企業組織を形作るための営みなのであって、重要な経営行為の1つであるように思います。そんな重大なイベントについて、このような詐欺的な行いが横行していては絶対にならない。

以上です。テクノブレーンさん側から何か申し開きがある場合はコメント欄からどうぞ。

*1:無いと思うけど、なにか仕事で関係あったっけ……と思って念のためかけたというのがあります、そういったメンタリティをハックしてきてるんだろうなあ

SORACOM ArcをESP32のArduinoで動かすsoracom-arc-esp32-arduinoのご紹介

こんにちは、株式会社ソラコムでソフトウェア等のエンジニアをやっているmoznionです。
普段ブログには書かない所属を宣言するのはなぜか。それはこれが株式会社ソラコム Advent Calendar 2021 17日目の記事だからです。というわけで記事が書かれます。

前日のアドベントカレンダー16日目は @0x6b さんによる soratun を改造して AWS Lambda から簡単に SORACOM Arc を使ってみました でした。そして本記事もSORACOM Arcの記事になります。連チャンしていて景気が良いですね。


表題の通り、SORACOM ArcをESP32のArduinoで動かすためのライブラリであるsoracom-arc-esp32-arduinoをご紹介します。
SORACOM Arcというサービスはザックリ説明すると「WireGuardを使ってデバイスとSORACOMプラットフォームとの間にトンネルを張り、そのトンネル越しに通信することでSORACOMのサービスを使えるようにするサービス」です。つまり従来は必須であって携帯電話回線を使わなくてもSORACOMサービスが使えるという接続サービスになります。

github.com

なお本ライブラリはArduinoLibrary Registryにも登録してありますので、ArduinoのLibrary Managerから利用が可能です。

f:id:moznion:20211217095811p:plain


使い方は至って簡単で、このライブラリ (SoracomArcESP32.h) をincludeしてちょっとしたコードを書いてやることですぐにSORACOM Arcを利用することができます。

#include <SoracomArcESP32.h>
#include <WiFi.h>
#include <HTTPClient.h>

#define BAUDRATE 115200

static SoracomArc soracomArc;

void setup() {
  Serial.begin(BAUDRATE);
  while (!Serial)
    ;

  WiFi.begin("__YOUR_AP_SSID__", "__YOUR_AP_PASSWORD__");
  while (!WiFi.isConnected()) {
    delay(1000);
  }

  configTime(0, 0, "ntp.nict.jp", "time.google.com", "pool.ntp.org");

  std::string soracomAPIAuthKeyID = "keyId-__YOUR_SORACOM_API_KEY_ID__";
  std::string soracomAPIAuthKey = "secret-__YOUR_SORACOM_API_AUTH_KEY__";
  std::string caCert = "-----BEGIN CERTIFICATE-----\n"
                       "...\n"
                       "__CA_CERTIFICATION_STRING__\n"
                       "...\n"
                       "-----END CERTIFICATE-----\n";

  SoracomAPI soracomAPIClient(soracomAPIAuthKeyID, soracomAPIAuthKey, caCert);

  std::string simID = "__YOUR_SIM_ID__";
  while (!soracomArc.activate(soracomAPIClient, simID)) {
    delay(5000);
  }

  log_i("setup finished");
}

void loop() {
  HTTPClient http;

  http.begin("http://metadata.soracom.io/");
  http.setConnectTimeout(1000);
  http.setTimeout(1000);
  int statusCode = http.GET();

  String payload = http.getString();
  log_i("http status: %d", statusCode);
  log_i("%s", payload.c_str());

  http.end();

  delay(30000);
}

このコードは以下のシークエンスに従って動作します。

1. Wi-Fiに接続する
2. NTPを用いてシステムの時刻合わせをする
3. SORACOM APIのauthKeyIdとauthKeyを組み合わせて認証を行ない、apiKeyとapiTokenを取得する
4. 上記のapiKey/apiTokenを用いて使用するArcのSIMについてセッションの再生成を行う。この再生成時にWireGuardのcredentialsを動的に発行する
5. 生成したArcのセッション情報を引いてくる
6. WireGuardのcredentials情報とセッション情報に基いてWireGuardトンネルを張る
7. Arcが有効に。上記の例では30秒ごとにメタデータサービスにアクセスし、SIMの情報を取得している

という感じです。コード中のCA certについては https://api.soracom.io に対するものを良い感じに埋め込んでください。


内部的には@cinimlさんのWireGuard-ESP32-Arduinoを使ってWireGuardの接続を行なっています。このライブラリの技術詳細についてはSORACOM Advent Calendar 2021 7日目の@cinimlさんによるWireGuard for ESP32の実装的なところという記事でつまびらかにされているので、併せて是非ご参照ください。


もちろん、WireGuardの設定情報をそのまま利用して利用することも可能です。その例はこちらにございます: https://github.com/soracom-labs/soracom-arc-esp32-arduino/tree/main/examples/boot-static-wg-credentials


既知の課題としては、SORACOM APIを使う方法にせよWireGuardの設定をそのまま使う方法にせよセンシティヴな情報をボード上で取り扱う必要が出てくるため、それらをセキュアな領域に置くあるいは何らかセキュアな方法で操作する必要があります。細心の注意を払っていただければと思います。


というわけでsoracom-arc-esp32-arduinoのご紹介でした。詳しい使い方についてはREADMEexamplesをご参照ください。
ESP32 ArduinoでSORACOM Arcが動作するとプロトタイピング等で活躍してくれそうで夢が広がりそうです。ぜひご利用ください!

おまけ

PlatformIOを使ってライブラリ開発してたんですけど、これ死ぬほど便利な一方ハマりどころがちらほらあるのでそういった落し穴について記録しておきます。

platform = espressif32を指定していると古いバージョンが降ってくる

これ新しいバージョンを明示しないと古いのを使い続けてしまいます。結構ドラスティックにプログラミングインターフェイスが変わってたりするんで、バージョン間違えると確実にハマります。

github.com

ここから欲しいバージョンを探してきて、 platform = espressif32@3.4.0 みたいな感じで指定してあげると良いです。

C++17使いたいんだけど……

という時はこのように platformio.ini に記述してやると良いです。

build_flags =
  -std=gnu++17
build_unflags =
  -std=gnu++11

残念ながら gnu++20 (C++20) はまだ対応していない様子。std::format()、とにかくバキバキに使いたんだが……