Linux で特定の時刻に実行するコマンドを作った

## 動機

次のような特定の日時に任意のスクリプトを実行できる [`at` コマンド](https://linuxjm.osdn.jp/html/at/man1/at.1.html)というものがある:

```sh
at [-V] [-q queue] [-f file] [-u username] [-mMlv] timespec
```

しかし次の点を不便に感じていた:
* スクリプトファイルを入力とするため、実行コマンドと引数を直接書けない
	* 対話的にジョブスクリプトが書けるのでそこでコマンドを書くことはできる
* 日付フォーマットが馴染まない
	* `MMDDYY` で書く必要があるが、日本人なら年月日の順で書きたい
* `atd` デーモンが必要で、ただ待ちたいだけなのに大袈裟

`sleep` コマンドや `timeout` コマンドのようなシンプルなものが欲しかった。

## 方針

`clock_nanosleep` ([man](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/clock_nanosleep.2.html)) という、指定日時まで待つまんまな API があるためこれを使う。
また、簡単のため日付のフォーマットは秒にする。日付フォーマットに不満を述べたばかりだが、日付を扱うのは簡単ではない。[gnulib](https://github.com/coreutils/gnulib/blob/master/lib/parse-datetime.y)を取り込めば `date` コマンドで使われている `parse_datetime` が手に入るが、拡張子が `.y` な時点で億劫だしライセンスも縛られてしまう。とりあえず秒にしておけば `date` コマンドで日付から秒に変換出来る。

コマンドの実行は `execvp` ([man](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/exec.3.html)) で行う。`argv` をコマンドの開始位置までずらせばいいだけなので簡単。

## 作った

`duntil @エポック秒 コマンド` で指定日時にコマンドを実行できる。
`duntil +ウエイト秒 コマンド` で一定の秒数待ったあとにコマンドを実行できる。

```cpp:duntil.cpp
#include <cerrno>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>

#include <time.h>
#include <unistd.h>

namespace {

	void usage()
	{
		std::puts("usage: duntil @POSIXTIME COMMAND [ARGS..]");
		std::puts("       duntil +SECONDS COMMAND [ARGS..]");
	}

	// double で表現可能な time_t の上限
	// time_t が32ビットの環境と64ビット環境で概ね動けば良い
	const double k_time_t_limit =
		double(std::numeric_limits<time_t>::max())
		* (1.0 - std::numeric_limits<double>::epsilon());

	// dateStr を解釈して t と isAbsolute を返す
	int parseDate(const char* dateStr, timespec& t, bool& isAbsolute)
	{
		switch ( dateStr[0] ) {
		case '+':
			isAbsolute = false;
			break;

		case '@':
			isAbsolute = true;
			break;

		default:
			return EINVAL;
		}
		dateStr += 1;

		char* next = nullptr;
		double secs = std::strtod(dateStr, &next);
		if ( errno == ERANGE ) {
			return ERANGE;
		}
		if ( next[0] != '\0' ) {
			return EINVAL;
		}

		double isecs = 0;
		double mod1 = std::modf(secs, &isecs);
		if ( isecs < 0 || k_time_t_limit < isecs ) {
			return ERANGE;
		}
		t.tv_sec = time_t(isecs);
		t.tv_nsec = long(mod1 * 1000000000.0);

		return 0;
	}

} // namespace

int main(int argc, char* argv[])
{
	if ( argc < 3 ) {
		usage();
		return 1;
	}

	const char* dateStr = argv[1];
	argv += 2;

	// 時刻を解釈
	timespec date{};
	bool isAbsolute = false;
	int err = parseDate(dateStr, date, isAbsolute);
	if ( err ) {
		std::fprintf(stderr, "%s\n", std::strerror(err));
		usage();
		return 1;
	}

	// 指定された時間まで待つ
	int flags = isAbsolute ? TIMER_ABSTIME : 0;
	if ( clock_nanosleep(CLOCK_REALTIME, flags, &date, NULL) ) {
		std::perror("clock_nanosleep");
		return 1;
	}

	// コマンドを実行
	int status = execvp(argv[0], argv);
	if ( status == -1 ) {
		std::fprintf(stderr, "%s\n", std::strerror(errno));
	}
	return status;
}
```

肝は `clock_nanosleep` の1行がやってくれるため、コードの大半は `parseDate` になっている。前述したように `parseDate` と言いつつ単位は秒だ。ここを拡張すれば多彩な日付フォーマットを受け付けることもできる。

## 使用例

2022年12月25日0:00になったら「Merry Christmas!」を出力する:

```sh
$ duntil @$(date +%s -d "2022-12-25 0:00") echo "Merry Christmas!"
```
日付を秒に直すのは `date` コマンドにやらせている。 

3分後に「ラーメン出来た！」を出力する:

```sh
$ duntil +180 echo "ラーメン出来た！"
```

## あとがき

`at` コマンドよりシンプルにした結果、[`at` + `atd` のコード](https://github.com/scfc/at) より1/10以下の分量で不満を大きく改善出来た。デーモンを動かす必要がないためセキュリティの心配も少ない。
`sleep` コマンドや `timeout` コマンドのように小粒で使いやすいものになったと思う。

上記ソースコードは https://github.com/yoffy/duntil で公開している。