LoginSignup
4
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-03

動機

次のような特定の日時に任意のスクリプトを実行できる at コマンドというものがある:

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

しかし次の点を不便に感じていた:

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

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

方針

clock_nanosleep (man) という、指定日時まで待つまんまな API があるためこれを使う。
また、簡単のため日付のフォーマットは秒にする。日付フォーマットに不満を述べたばかりだが、日付を扱うのは簡単ではない。gnulibを取り込めば date コマンドで使われている parse_datetime が手に入るが、拡張子が .y な時点で億劫だしライセンスも縛られてしまう。とりあえず秒にしておけば date コマンドで日付から秒に変換出来る。

コマンドの実行は execvp (man) で行う。argv をコマンドの開始位置までずらせばいいだけなので簡単。

作った

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

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!」を出力する:

$ duntil @$(date +%s -d "2022-12-25 0:00") echo "Merry Christmas!"

日付を秒に直すのは date コマンドにやらせている。

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

$ duntil +180 echo "ラーメン出来た!"

あとがき

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

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

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0